| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- <template>
- <div class="p-6 flex justify-center min-h-screen">
- <div class="w-full mx-auto">
- <!-- 顶部筛选区 -->
- <div class="bg-white rounded-lg shadow-sm p-6 mb-6 h-25 flex items-center">
- <!-- 讲座选择 -->
- <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 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>
- </div>
- </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>
- </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>
- <!-- 操作按钮 -->
- <div class="flex space-x-2 ml-4 mt-6">
- <a-button @click="refreshData" :loading="loading">
- <template #icon>
- <ReloadOutlined />
- </template>
- 刷新
- </a-button>
- </div>
- </div>
- <!-- 核心数据看板 -->
- <div class="mb-6">
- <!-- 数据卡片 -->
- <div class="grid grid-cols-4 gap-6 mb-6">
- <div
- v-for="(card, index) in statsCards"
- :key="index"
- class="card-header bg-white p-5 rounded-lg shadow-sm"
- :class="card.borderClass"
- >
- <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>
- <div :class="card.iconBgClass" class="p-2 rounded-full">
- <component :is="card.icon" :class="card.iconClass" class="text-xl" />
- </div>
- </div>
- </div>
- </div>
- <!-- 图表区 -->
- <div class="grid grid-cols-2 gap-6">
- <!-- 折线图 -->
- <div class="chart-container p-4 bg-white border border-gray-200 rounded">
- <h3 class="font-bold text-gray-800 mb-4">访问人数趋势</h3>
- <div ref="lineChartRef" class="h-48"></div>
- </div>
- <!-- 柱状图 -->
- <div class="chart-container p-4 bg-white border border-gray-200 rounded">
- <h3 class="font-bold text-gray-800 mb-4">练习平均提交数</h3>
- <div ref="barChartRef" class="h-48"></div>
- </div>
- </div>
- </div>
- <!-- 明细表格区 -->
- <div class="bg-white rounded-lg shadow-sm p-6">
- <div class="flex justify-between items-center mb-4">
- <h2 class="text-xl font-bold text-gray-800">学习明细数据</h2>
- <div class="text-sm text-gray-500">({{ formatDateRange() }})注:从查询条件时间范围落下来的</div>
- </div>
- <a-table
- :columns="tableColumns"
- :data-source="tableData"
- :pagination="pagination"
- :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>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
- import { ReloadOutlined, UserOutlined, EyeOutlined, FileTextOutlined, MessageOutlined } from '@ant-design/icons-vue'
- import * as echarts from 'echarts'
- import dayjs from 'dayjs'
- // 响应式数据
- const loading = ref(false)
- const lineChartRef = ref(null)
- const barChartRef = ref(null)
- let lineChart = null
- let barChart = null
- // 筛选条件
- const filters = reactive({
- courseName: '',
- contentType: 'all',
- dateRange: [dayjs('2025-08-04'), dayjs('2025-08-10')]
- })
- // 内容类型选项
- const contentTypes = [
- { label: '全部', value: 'all' },
- { label: '视频', value: 'video' },
- { label: '文档', value: 'document' },
- { label: '练习', value: 'exercise' }
- ]
- // 统计卡片数据
- const statsCards = reactive([
- {
- title: '开课人数',
- value: 2846,
- icon: UserOutlined,
- borderClass: 'border-l-4 !border-blue-500',
- iconBgClass: 'bg-blue-100',
- iconClass: 'text-blue-600'
- },
- {
- title: '课程访问次数',
- value: 12587,
- icon: EyeOutlined,
- borderClass: 'border-l-4 !border-green-500',
- iconBgClass: 'bg-green-100',
- iconClass: 'text-green-600'
- },
- {
- title: '提交数(作业、测验)',
- value: 1924,
- icon: FileTextOutlined,
- borderClass: 'border-l-4 !border-orange-500',
- iconBgClass: 'bg-orange-100',
- iconClass: 'text-orange-500'
- },
- {
- title: '互动数(发帖/回帖)',
- value: 5362,
- icon: MessageOutlined,
- borderClass: 'border-l-4 !border-blue-500',
- iconBgClass: 'bg-blue-100',
- iconClass: 'text-blue-600'
- }
- ])
- const tableColumns = [
- {
- title: '讲座名称',
- dataIndex: 'courseName',
- key: 'courseName',
- sorter: true
- },
- {
- title: '开课人数',
- dataIndex: 'studentCount',
- key: 'studentCount',
- sorter: true
- },
- {
- title: '课程访问量',
- dataIndex: 'visitCount',
- key: 'visitCount',
- sorter: true
- },
- {
- title: '学习视频总时长',
- dataIndex: 'videoDuration',
- key: 'videoDuration',
- sorter: true
- },
- {
- title: '讲义访问量',
- dataIndex: 'documentVisit',
- key: 'documentVisit',
- sorter: true
- },
- {
- title: '练习提交量',
- dataIndex: 'submissionCount',
- key: 'submissionCount',
- 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 pagination = reactive({
- current: 1,
- pageSize: 6,
- total: 32,
- showSizeChanger: true,
- showQuickJumper: true,
- showTotal: (total, range) => `显示 ${range[0]}-${range[1]} 条,共 ${total} 条`
- })
- // 初始化折线图
- const initLineChart = () => {
- if (!lineChartRef.value) return
- lineChart = echarts.init(lineChartRef.value)
- 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日']
- },
- yAxis: { type: 'value', name: '访问人数' },
- series: [
- {
- name: '周访问人数',
- type: 'line',
- smooth: true,
- symbol: 'circle',
- symbolSize: 8,
- data: [2150, 2380, 1920, 2650, 2210, 2490, 2000],
- lineStyle: { color: '#3A7BFF', width: 3 },
- itemStyle: { color: '#3A7BFF' },
- areaStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: 'rgba(58, 123, 255, 0.3)' },
- { offset: 1, color: 'rgba(58, 123, 255, 0.05)' }
- ])
- }
- }
- ]
- }
- lineChart.setOption(option)
- }
- // 初始化柱状图
- const initBarChart = () => {
- if (!barChartRef.value) return
- barChart = echarts.init(barChartRef.value)
- 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日']
- },
- yAxis: { type: 'value', name: '平均提交数' },
- series: [
- {
- name: '练习平均提交数',
- type: 'bar',
- barWidth: 28,
- data: [82, 85, 78, 89, 76, 91, 83],
- itemStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: '#5B8EFF' },
- { offset: 1, color: '#3A7BFF' }
- ]),
- borderRadius: [4, 4, 0, 0]
- }
- }
- ]
- }
- barChart.setOption(option)
- }
- // 格式化日期范围显示
- 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')
- return `${start}至${end}`
- }
- // 刷新数据
- const refreshData = async () => {
- loading.value = true
- try {
- // 模拟API调用
- await new Promise((resolve) => setTimeout(resolve, 1000))
- // 这里可以调用实际的API来获取数据
- console.log('刷新数据', filters)
- } catch (error) {
- console.error('刷新数据失败:', error)
- } finally {
- loading.value = false
- }
- }
- // 窗口大小变化处理
- const handleResize = () => {
- if (lineChart) lineChart.resize()
- if (barChart) barChart.resize()
- }
- // 生命周期
- onMounted(async () => {
- await nextTick()
- initLineChart()
- initBarChart()
- window.addEventListener('resize', handleResize)
- })
- onUnmounted(() => {
- if (lineChart) {
- lineChart.dispose()
- lineChart = null
- }
- if (barChart) {
- barChart.dispose()
- barChart = null
- }
- window.removeEventListener('resize', handleResize)
- })
- </script>
- <style scoped>
- .card-header {
- border-left: 4px solid;
- }
- .chart-container {
- background-color: white;
- border: 1px solid #e4e7ed;
- border-radius: 4px;
- }
- </style>
|