index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. <template>
  2. <div class="p-6 flex justify-center min-h-screen">
  3. <div class="w-full mx-auto">
  4. <!-- 顶部筛选区 -->
  5. <div class="bg-white rounded-lg shadow-sm p-6 mb-6 h-25 flex items-center">
  6. <!-- 讲座选择 -->
  7. <div class="flex-1 mr-4">
  8. <label class="block text-sm font-medium text-gray-700 mb-1">讲座名称</label>
  9. <div class="relative">
  10. <a-select v-model:value="filters.courseName" class="w-full" placeholder="全部课程">
  11. <a-select-option value="">全部课程</a-select-option>
  12. <a-select-option value="python">Python编程入门</a-select-option>
  13. <a-select-option value="datastructure">数据结构与算法</a-select-option>
  14. <a-select-option value="ml">机器学习基础</a-select-option>
  15. <a-select-option value="frontend">Web前端开发</a-select-option>
  16. </a-select>
  17. </div>
  18. </div>
  19. <!-- 内容类型 -->
  20. <div class="flex-1 mr-4">
  21. <label class="block text-sm font-medium text-gray-700 mb-1">内容类型</label>
  22. <div class="flex space-x-2">
  23. <a-button
  24. v-for="type in contentTypes"
  25. :key="type.value"
  26. :type="filters.contentType === type.value ? 'primary' : 'default'"
  27. size="small"
  28. @click="filters.contentType = type.value"
  29. >
  30. {{ type.label }}
  31. </a-button>
  32. </div>
  33. </div>
  34. <!-- 日期范围选择 -->
  35. <div class="flex-1">
  36. <label class="block text-sm font-medium text-gray-700 mb-1">日期范围(周次)</label>
  37. <a-range-picker v-model:value="filters.dateRange" class="w-full" :placeholder="['开始日期', '结束日期']" />
  38. </div>
  39. <!-- 操作按钮 -->
  40. <div class="flex space-x-2 ml-4 mt-6">
  41. <a-button @click="refreshData" :loading="loading">
  42. <template #icon>
  43. <ReloadOutlined />
  44. </template>
  45. 刷新
  46. </a-button>
  47. </div>
  48. </div>
  49. <!-- 核心数据看板 -->
  50. <div class="mb-6">
  51. <!-- 数据卡片 -->
  52. <div class="grid grid-cols-4 gap-6 mb-6">
  53. <div
  54. v-for="(card, index) in statsCards"
  55. :key="index"
  56. class="card-header bg-white p-5 rounded-lg shadow-sm"
  57. :class="card.borderClass"
  58. >
  59. <div class="flex justify-between items-start">
  60. <div>
  61. <div class="text-gray-500 text-sm">{{ card.title }}</div>
  62. <div class="text-4xl font-bold text-gray-800 mt-2">{{ card.value.toLocaleString() }}</div>
  63. </div>
  64. <div :class="card.iconBgClass" class="p-2 rounded-full">
  65. <component :is="card.icon" :class="card.iconClass" class="text-xl" />
  66. </div>
  67. </div>
  68. </div>
  69. </div>
  70. <!-- 图表区 -->
  71. <div class="grid grid-cols-2 gap-6">
  72. <!-- 折线图 -->
  73. <div class="chart-container p-4 bg-white border border-gray-200 rounded">
  74. <h3 class="font-bold text-gray-800 mb-4">访问人数趋势</h3>
  75. <div ref="lineChartRef" class="h-48"></div>
  76. </div>
  77. <!-- 柱状图 -->
  78. <div class="chart-container p-4 bg-white border border-gray-200 rounded">
  79. <h3 class="font-bold text-gray-800 mb-4">练习平均提交数</h3>
  80. <div ref="barChartRef" class="h-48"></div>
  81. </div>
  82. </div>
  83. </div>
  84. <!-- 明细表格区 -->
  85. <div class="bg-white rounded-lg shadow-sm p-6">
  86. <div class="flex justify-between items-center mb-4">
  87. <h2 class="text-xl font-bold text-gray-800">学习明细数据</h2>
  88. <div class="text-sm text-gray-500">({{ formatDateRange() }})注:从查询条件时间范围落下来的</div>
  89. </div>
  90. <a-table
  91. :columns="tableColumns"
  92. :data-source="tableData"
  93. :pagination="pagination"
  94. :loading="loading"
  95. size="small"
  96. >
  97. <template #bodyCell="{ column, record }">
  98. <template v-if="column.key === 'submissionCount'">
  99. <span
  100. :class="{
  101. 'text-green-500': record.submissionCount >= 90,
  102. 'text-orange-500': record.submissionCount >= 70 && record.submissionCount < 90,
  103. 'text-red-500': record.submissionCount < 70
  104. }"
  105. class="font-semibold"
  106. >
  107. {{ record.submissionCount }}
  108. </span>
  109. </template>
  110. </template>
  111. </a-table>
  112. </div>
  113. </div>
  114. </div>
  115. </template>
  116. <script setup>
  117. import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
  118. import { ReloadOutlined, UserOutlined, EyeOutlined, FileTextOutlined, MessageOutlined } from '@ant-design/icons-vue'
  119. import * as echarts from 'echarts'
  120. import dayjs from 'dayjs'
  121. // 响应式数据
  122. const loading = ref(false)
  123. const lineChartRef = ref(null)
  124. const barChartRef = ref(null)
  125. let lineChart = null
  126. let barChart = null
  127. // 筛选条件
  128. const filters = reactive({
  129. courseName: '',
  130. contentType: 'all',
  131. dateRange: [dayjs('2025-08-04'), dayjs('2025-08-10')]
  132. })
  133. // 内容类型选项
  134. const contentTypes = [
  135. { label: '全部', value: 'all' },
  136. { label: '视频', value: 'video' },
  137. { label: '文档', value: 'document' },
  138. { label: '练习', value: 'exercise' }
  139. ]
  140. // 统计卡片数据
  141. const statsCards = reactive([
  142. {
  143. title: '开课人数',
  144. value: 2846,
  145. icon: UserOutlined,
  146. borderClass: 'border-l-4 !border-blue-500',
  147. iconBgClass: 'bg-blue-100',
  148. iconClass: 'text-blue-600'
  149. },
  150. {
  151. title: '课程访问次数',
  152. value: 12587,
  153. icon: EyeOutlined,
  154. borderClass: 'border-l-4 !border-green-500',
  155. iconBgClass: 'bg-green-100',
  156. iconClass: 'text-green-600'
  157. },
  158. {
  159. title: '提交数(作业、测验)',
  160. value: 1924,
  161. icon: FileTextOutlined,
  162. borderClass: 'border-l-4 !border-orange-500',
  163. iconBgClass: 'bg-orange-100',
  164. iconClass: 'text-orange-500'
  165. },
  166. {
  167. title: '互动数(发帖/回帖)',
  168. value: 5362,
  169. icon: MessageOutlined,
  170. borderClass: 'border-l-4 !border-blue-500',
  171. iconBgClass: 'bg-blue-100',
  172. iconClass: 'text-blue-600'
  173. }
  174. ])
  175. const tableColumns = [
  176. {
  177. title: '讲座名称',
  178. dataIndex: 'courseName',
  179. key: 'courseName',
  180. sorter: true
  181. },
  182. {
  183. title: '开课人数',
  184. dataIndex: 'studentCount',
  185. key: 'studentCount',
  186. sorter: true
  187. },
  188. {
  189. title: '课程访问量',
  190. dataIndex: 'visitCount',
  191. key: 'visitCount',
  192. sorter: true
  193. },
  194. {
  195. title: '学习视频总时长',
  196. dataIndex: 'videoDuration',
  197. key: 'videoDuration',
  198. sorter: true
  199. },
  200. {
  201. title: '讲义访问量',
  202. dataIndex: 'documentVisit',
  203. key: 'documentVisit',
  204. sorter: true
  205. },
  206. {
  207. title: '练习提交量',
  208. dataIndex: 'submissionCount',
  209. key: 'submissionCount',
  210. sorter: true
  211. },
  212. {
  213. title: '退课人数',
  214. dataIndex: 'numberStudentsDroppingCourses',
  215. key: 'numberStudentsDroppingCourses',
  216. sorter: true
  217. }
  218. ]
  219. // 表格数据
  220. const tableData = ref([
  221. {
  222. key: '1',
  223. courseName: 'Python编程基础',
  224. studentCount: 60,
  225. visitCount: 1245,
  226. videoDuration: '5h12m30s',
  227. documentVisit: 453,
  228. submissionCount: 94,
  229. numberStudentsDroppingCourses: 10
  230. },
  231. {
  232. key: '2',
  233. courseName: '数据结构 - 栈与队列',
  234. studentCount: 70,
  235. visitCount: 1087,
  236. videoDuration: '5h12m30s',
  237. documentVisit: 389,
  238. submissionCount: 88,
  239. numberStudentsDroppingCourses: 12
  240. },
  241. {
  242. key: '3',
  243. courseName: 'Web前端框架实战',
  244. studentCount: 80,
  245. visitCount: 987,
  246. videoDuration: '5h12m30s',
  247. documentVisit: 326,
  248. submissionCount: 79,
  249. numberStudentsDroppingCourses: 15
  250. },
  251. {
  252. key: '4',
  253. courseName: '机器学习入门',
  254. studentCount: 90,
  255. visitCount: 856,
  256. videoDuration: '5h12m30s',
  257. documentVisit: 278,
  258. submissionCount: 75,
  259. numberStudentsDroppingCourses: 18
  260. },
  261. {
  262. key: '5',
  263. courseName: '数据库设计原理',
  264. studentCount: 100,
  265. visitCount: 723,
  266. videoDuration: '5h12m30s',
  267. documentVisit: 214,
  268. submissionCount: 72,
  269. numberStudentsDroppingCourses: 20
  270. },
  271. {
  272. key: '6',
  273. courseName: '算法复杂度分析',
  274. studentCount: 110,
  275. visitCount: 634,
  276. videoDuration: '5h12m30s',
  277. documentVisit: 196,
  278. submissionCount: 68,
  279. numberStudentsDroppingCourses: 22
  280. }
  281. ])
  282. // 分页配置
  283. const pagination = reactive({
  284. current: 1,
  285. pageSize: 6,
  286. total: 32,
  287. showSizeChanger: true,
  288. showQuickJumper: true,
  289. showTotal: (total, range) => `显示 ${range[0]}-${range[1]} 条,共 ${total} 条`
  290. })
  291. // 初始化折线图
  292. const initLineChart = () => {
  293. if (!lineChartRef.value) return
  294. lineChart = echarts.init(lineChartRef.value)
  295. const option = {
  296. grid: { top: 30, right: 20, bottom: 20, left: 40 },
  297. tooltip: { trigger: 'axis' },
  298. xAxis: {
  299. type: 'category',
  300. data: ['4日', '5日', '6日', '7日', '8日', '9日', '10日']
  301. },
  302. yAxis: { type: 'value', name: '访问人数' },
  303. series: [
  304. {
  305. name: '周访问人数',
  306. type: 'line',
  307. smooth: true,
  308. symbol: 'circle',
  309. symbolSize: 8,
  310. data: [2150, 2380, 1920, 2650, 2210, 2490, 2000],
  311. lineStyle: { color: '#3A7BFF', width: 3 },
  312. itemStyle: { color: '#3A7BFF' },
  313. areaStyle: {
  314. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  315. { offset: 0, color: 'rgba(58, 123, 255, 0.3)' },
  316. { offset: 1, color: 'rgba(58, 123, 255, 0.05)' }
  317. ])
  318. }
  319. }
  320. ]
  321. }
  322. lineChart.setOption(option)
  323. }
  324. // 初始化柱状图
  325. const initBarChart = () => {
  326. if (!barChartRef.value) return
  327. barChart = echarts.init(barChartRef.value)
  328. const option = {
  329. grid: { top: 30, right: 20, bottom: 20, left: 40 },
  330. tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  331. xAxis: {
  332. type: 'category',
  333. data: ['4日', '5日', '6日', '7日', '8日', '9日', '10日']
  334. },
  335. yAxis: { type: 'value', name: '平均提交数' },
  336. series: [
  337. {
  338. name: '练习平均提交数',
  339. type: 'bar',
  340. barWidth: 28,
  341. data: [82, 85, 78, 89, 76, 91, 83],
  342. itemStyle: {
  343. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  344. { offset: 0, color: '#5B8EFF' },
  345. { offset: 1, color: '#3A7BFF' }
  346. ]),
  347. borderRadius: [4, 4, 0, 0]
  348. }
  349. }
  350. ]
  351. }
  352. barChart.setOption(option)
  353. }
  354. // 格式化日期范围显示
  355. const formatDateRange = () => {
  356. if (!filters.dateRange || filters.dateRange.length !== 2) {
  357. return '2025/08/04至2025/08/10'
  358. }
  359. const start = filters.dateRange[0].format('YYYY/MM/DD')
  360. const end = filters.dateRange[1].format('YYYY/MM/DD')
  361. return `${start}至${end}`
  362. }
  363. // 刷新数据
  364. const refreshData = async () => {
  365. loading.value = true
  366. try {
  367. // 模拟API调用
  368. await new Promise((resolve) => setTimeout(resolve, 1000))
  369. // 这里可以调用实际的API来获取数据
  370. console.log('刷新数据', filters)
  371. } catch (error) {
  372. console.error('刷新数据失败:', error)
  373. } finally {
  374. loading.value = false
  375. }
  376. }
  377. // 窗口大小变化处理
  378. const handleResize = () => {
  379. if (lineChart) lineChart.resize()
  380. if (barChart) barChart.resize()
  381. }
  382. // 生命周期
  383. onMounted(async () => {
  384. await nextTick()
  385. initLineChart()
  386. initBarChart()
  387. window.addEventListener('resize', handleResize)
  388. })
  389. onUnmounted(() => {
  390. if (lineChart) {
  391. lineChart.dispose()
  392. lineChart = null
  393. }
  394. if (barChart) {
  395. barChart.dispose()
  396. barChart = null
  397. }
  398. window.removeEventListener('resize', handleResize)
  399. })
  400. </script>
  401. <style scoped>
  402. .card-header {
  403. border-left: 4px solid;
  404. }
  405. .chart-container {
  406. background-color: white;
  407. border: 1px solid #e4e7ed;
  408. border-radius: 4px;
  409. }
  410. </style>