index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. <template>
  2. <div class="p-6 flex justify-center">
  3. <div class="w-full mx-auto min-h-screen">
  4. <!-- 顶部筛选区 -->
  5. <div class="bg-white rounded-lg shadow-sm p-6 mb-6 h-25 flex items-center">
  6. <!-- 课程ID输入 -->
  7. <div class="flex-1 mr-4">
  8. <label class="block text-sm font-medium text-gray-700 mb-1">课程ID</label>
  9. <a-select class="w-full" v-model:value="filters.courseId" placeholder="请选择课程" allow-clear>
  10. <a-select-option v-for="item in courseinfoAllListOptions" :key="item.courseId" :value="item.courseId">
  11. {{ item.courseName }}
  12. </a-select-option>
  13. </a-select>
  14. </div>
  15. <!-- 日期范围选择 -->
  16. <!-- <div class="flex-1 mr-4">-->
  17. <!-- <label class="block text-sm font-medium text-gray-700 mb-1">开始时间</label>-->
  18. <!-- <a-date-picker v-model:value="startDate" class="w-full" placeholder="开始时间" />-->
  19. <!-- </div>-->
  20. <!-- <div class="flex-1 mr-4">-->
  21. <!-- <label class="block text-sm font-medium text-gray-700 mb-1">结束时间</label>-->
  22. <!-- <a-date-picker v-model:value="endDate" class="w-full" placeholder="结束时间" />-->
  23. <!-- </div>-->
  24. <!-- 操作按钮 -->
  25. <div class="flex space-x-2 ml-4 mt-6">
  26. <a-button @click="refreshData" :loading="loading">
  27. <template #icon>
  28. <ReloadOutlined />
  29. </template>
  30. 刷新
  31. </a-button>
  32. </div>
  33. </div>
  34. <!-- 核心数据看板 -->
  35. <div class="mb-6">
  36. <!-- 数据卡片 -->
  37. <div class="grid grid-cols-4 gap-6 mb-6">
  38. <div
  39. v-for="(card, index) in statsCards"
  40. :key="index"
  41. class="card-header bg-white p-5 rounded-lg shadow-sm"
  42. :class="card.borderClass"
  43. >
  44. <div class="flex justify-between items-start">
  45. <div>
  46. <div class="text-gray-500 text-sm">{{ card.title }}</div>
  47. <div class="font-size-30 font-bold text-gray-800 mt-2">{{ card.value.toLocaleString() }}</div>
  48. </div>
  49. <div :class="card.iconBgClass" class="p-2 rounded-full">
  50. <component :is="card.icon" :class="card.iconClass" class="text-xl" />
  51. </div>
  52. </div>
  53. </div>
  54. </div>
  55. <!-- 图表区 -->
  56. <div class="grid grid-cols-2 gap-6">
  57. <!-- 折线图 -->
  58. <div class="chart-container p-4 bg-white border border-gray-200 rounded">
  59. <h3 class="font-bold text-gray-800 mb-4">访问人数趋势</h3>
  60. <div ref="lineChartRef" class="h-48"></div>
  61. </div>
  62. <!-- 柱状图 -->
  63. <div class="chart-container p-4 bg-white border border-gray-200 rounded">
  64. <h3 class="font-bold text-gray-800 mb-4">练习平均提交数</h3>
  65. <div ref="barChartRef" class="h-48"></div>
  66. </div>
  67. </div>
  68. </div>
  69. <!-- 明细表格区 -->
  70. <div class="bg-white rounded-lg shadow-sm p-6">
  71. <div class="flex justify-between items-center mb-4">
  72. <h2 class="text-xl font-bold text-gray-800">学习明细数据</h2>
  73. <!-- <div class="text-sm text-gray-500">({{ formatDateRange() }})注:从查询条件时间范围落下来的</div>-->
  74. </div>
  75. <a-table
  76. :columns="tableColumns"
  77. :data-source="tableData"
  78. :pagination="pagination"
  79. :loading="loading"
  80. size="small"
  81. >
  82. <template #bodyCell="{ column, text, record }">
  83. <!-- 状态列 -->
  84. <template v-if="column.key === 'viewCount'" >
  85. <span @click="onItemStudyDetailCourseView(record)">
  86. {{text}}
  87. </span>
  88. </template>
  89. <template v-if="column.key === 'paperSubmitNum'" >
  90. <span @click="onItemStudyDetailPracticeResult(record)">
  91. {{text}}
  92. </span>
  93. </template>
  94. </template>
  95. </a-table>
  96. </div>
  97. </div>
  98. </div>
  99. <ListViewlistViewStudyDetailCourseView ref="listViewListViewlistViewStudyDetailCourseViewRef"></ListViewlistViewStudyDetailCourseView>
  100. <ListViewStudyDetailPracticeResult ref="listViewStudyDetailPracticeResultRef"></ListViewStudyDetailPracticeResult>
  101. </template>
  102. <script setup>
  103. import { ref, reactive, onMounted, onUnmounted, nextTick, watch } from 'vue'
  104. import { ReloadOutlined, UserOutlined, EyeOutlined, FileTextOutlined, MessageOutlined } from '@ant-design/icons-vue'
  105. import * as echarts from 'echarts'
  106. import dayjs from 'dayjs'
  107. import { overviewLearningProgressApi } from '@/api/statisticalAnalysis/overviewLearningProgress'
  108. import { courseinfoAllList } from '@/api/semester/index.js'
  109. import ListViewlistViewStudyDetailCourseView from './listViewStudyDetailCourseView.vue'
  110. import ListViewStudyDetailPracticeResult from './listViewStudyDetailPracticeResult.vue'
  111. import { message } from 'ant-design-vue'
  112. // 响应式数据
  113. const loading = ref(false)
  114. const lineChartRef = ref(null)
  115. const listViewListViewlistViewStudyDetailCourseViewRef = ref(null)
  116. const listViewStudyDetailPracticeResultRef = ref(null)
  117. const barChartRef = ref(null)
  118. const courseinfoAllListOptions = ref([])
  119. let lineChart = null
  120. let barChart = null
  121. // 日期选择器
  122. // const startDate = ref(dayjs('2025-08-04'))
  123. // const endDate = ref(dayjs('2025-08-10'))
  124. const startDate = ref(undefined)
  125. const endDate = ref(undefined)
  126. // 筛选条件
  127. const filters = reactive({
  128. courseId: '',
  129. startTime: undefined,
  130. endTime: undefined,
  131. })
  132. // 分页配置
  133. const pagination = reactive({
  134. current: 1,
  135. size: 10,
  136. total: 0,
  137. showSizeChanger: true,
  138. showQuickJumper: true,
  139. showTotal: (total, range) => `显示 ${range[0]}-${range[1]} 条,共 ${total} 条`,
  140. onChange: (page, pageSize) => {
  141. pagination.current = page
  142. pagination.size = pageSize
  143. fetchStudyDetail()
  144. }
  145. })
  146. // 统计卡片数据
  147. const statsCards = reactive([
  148. {
  149. title: '开课人数',
  150. value: 0,
  151. key: 'courseOpenStuNum',
  152. icon: UserOutlined,
  153. borderClass: 'border-l-4 !border-blue-500',
  154. iconBgClass: 'bg-blue-100',
  155. iconClass: 'text-blue-600'
  156. },
  157. {
  158. title: '课程访问次数',
  159. value: 0,
  160. key: 'courseViewNum',
  161. icon: EyeOutlined,
  162. borderClass: 'border-l-4 !border-green-500',
  163. iconBgClass: 'bg-green-100',
  164. iconClass: 'text-green-600'
  165. },
  166. {
  167. title: '提交数(作业、测验)',
  168. value: 0,
  169. key: 'paperSubmitNum',
  170. icon: FileTextOutlined,
  171. borderClass: 'border-l-4 !border-orange-500',
  172. iconBgClass: 'bg-orange-100',
  173. iconClass: 'text-orange-500'
  174. },
  175. {
  176. title: '互动数(发帖/回帖)',
  177. value: 0,
  178. key: 'interactionNum',
  179. icon: MessageOutlined,
  180. borderClass: 'border-l-4 !border-blue-500',
  181. iconBgClass: 'bg-blue-100',
  182. iconClass: 'text-blue-600'
  183. }
  184. ])
  185. const tableColumns = [
  186. {
  187. title: '课程名称',
  188. dataIndex: 'courseName',
  189. key: 'courseName'
  190. },
  191. {
  192. title: '开课人数',
  193. dataIndex: 'openStuNum',
  194. key: 'openStuNum'
  195. },
  196. {
  197. title: '课程访问量',
  198. dataIndex: 'viewCount',
  199. key: 'viewCount'
  200. },
  201. {
  202. title: '讲义访问量',
  203. dataIndex: 'teachMaterialsNum',
  204. key: 'teachMaterialsNum'
  205. },
  206. {
  207. title: '练习提交量',
  208. dataIndex: 'paperSubmitNum',
  209. key: 'paperSubmitNum'
  210. }
  211. ]
  212. // 图表数据
  213. const chartData = reactive({
  214. visitTrend: [],
  215. submissionTrend: []
  216. })
  217. // 表格数据
  218. const tableData = ref([])
  219. const onItemStudyDetailCourseView = (record) => {
  220. // console.log('123123点了什么123123',record,' listViewRef ',listViewRef)
  221. listViewListViewlistViewStudyDetailCourseViewRef.value.open(record.courseId)
  222. }
  223. const onItemStudyDetailPracticeResult = (record) => {
  224. listViewStudyDetailPracticeResultRef.value.open(record.courseId)
  225. // console.log('点了什么',record)
  226. // const params = {
  227. // courseId: record.courseId,
  228. // startTime: undefined,
  229. // endTime: undefined,
  230. // size : 99999,
  231. // current : 1
  232. // }
  233. // overviewLearningProgressApi.getStudyDetailPracticeResult(params).then((res)=>{
  234. //
  235. // })
  236. }
  237. // 初始化折线图
  238. const initLineChart = () => {
  239. if (!lineChartRef.value) return
  240. lineChart = echarts.init(lineChartRef.value)
  241. updateLineChart()
  242. }
  243. // 更新折线图数据
  244. const updateLineChart = () => {
  245. if (!lineChart) return
  246. const option = {
  247. grid: { top: 30, right: 20, bottom: 20, left: 40 },
  248. tooltip: { trigger: 'axis' },
  249. xAxis: {
  250. type: 'category',
  251. data: chartData.visitTrend.map((item) => item.month)
  252. },
  253. yAxis: { type: 'value', name: '访问人数' },
  254. series: [
  255. {
  256. name: '访问人数',
  257. type: 'line',
  258. smooth: true,
  259. symbol: 'circle',
  260. symbolSize: 8,
  261. data: chartData.visitTrend.map((item) => item.viewTendencyNum),
  262. lineStyle: { color: '#3A7BFF', width: 3 },
  263. itemStyle: { color: '#3A7BFF' },
  264. areaStyle: {
  265. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  266. { offset: 0, color: 'rgba(58, 123, 255, 0.3)' },
  267. { offset: 1, color: 'rgba(58, 123, 255, 0.05)' }
  268. ])
  269. }
  270. }
  271. ]
  272. }
  273. lineChart.setOption(option)
  274. }
  275. // 初始化柱状图
  276. const initBarChart = () => {
  277. if (!barChartRef.value) return
  278. barChart = echarts.init(barChartRef.value)
  279. updateBarChart()
  280. }
  281. // 更新柱状图数据
  282. const updateBarChart = () => {
  283. if (!barChart) return
  284. const option = {
  285. grid: { top: 30, right: 20, bottom: 20, left: 40 },
  286. tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  287. xAxis: {
  288. type: 'category',
  289. data: chartData.submissionTrend.map((item) => item.month)
  290. },
  291. yAxis: { type: 'value', name: '提交数' },
  292. series: [
  293. {
  294. name: '练习提交数',
  295. type: 'bar',
  296. barWidth: 28,
  297. data: chartData.submissionTrend.map((item) => item.SubmeitTendency),
  298. itemStyle: {
  299. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  300. { offset: 0, color: '#5B8EFF' },
  301. { offset: 1, color: '#3A7BFF' }
  302. ]),
  303. borderRadius: [4, 4, 0, 0]
  304. }
  305. }
  306. ]
  307. }
  308. barChart.setOption(option)
  309. }
  310. // 格式化日期范围显示
  311. const formatDateRange = () => {
  312. const start = startDate.value ? startDate.value.format('YYYY/MM/DD') : filters.startTime
  313. const end = endDate.value ? endDate.value.format('YYYY/MM/DD') : filters.endTime
  314. return `${start}至${end}`
  315. }
  316. // 获取统计概览数据
  317. const fetchOverviewStats = async () => {
  318. try {
  319. const params = {
  320. courseId: filters.courseId,
  321. startTime: startDate.value ? startDate.value.format('YYYY-MM-DD') : filters.startTime,
  322. endTime: endDate.value ? endDate.value.format('YYYY-MM-DD') : filters.endTime
  323. }
  324. const data = await overviewLearningProgressApi.getTopFundamentalDetail(params)
  325. // 更新统计卡片数据
  326. statsCards.forEach((card) => {
  327. card.value = data[card.key] || 0
  328. })
  329. } catch (error) {
  330. console.error('获取统计数据失败:', error)
  331. message.error('获取统计数据失败')
  332. }
  333. }
  334. // 获取趋势数据
  335. const fetchTrendData = async () => {
  336. try {
  337. const params = {
  338. courseId: filters.courseId,
  339. startTime: startDate.value ? startDate.value.format('YYYY-MM-DD') : filters.startTime,
  340. endTime: endDate.value ? endDate.value.format('YYYY-MM-DD') : filters.endTime
  341. }
  342. // 并行获取访问趋势和提交趋势数据
  343. const [visitData, submitData] = await Promise.all([
  344. overviewLearningProgressApi.getViewTendency(params),
  345. overviewLearningProgressApi.getPaperSubmitTendency(params)
  346. ])
  347. // 更新图表数据
  348. chartData.visitTrend = visitData || []
  349. chartData.submissionTrend = submitData || []
  350. // 更新图表
  351. updateLineChart()
  352. updateBarChart()
  353. } catch (error) {
  354. console.error('获取趋势数据失败:', error)
  355. message.error('获取趋势数据失败')
  356. }
  357. }
  358. // 获取学习明细数据
  359. const fetchStudyDetail = async () => {
  360. try {
  361. const params = {
  362. courseId: filters.courseId,
  363. current : pagination.current,
  364. size : pagination.size,
  365. startTime: startDate.value ? startDate.value.format('YYYY-MM-DD') : filters.startTime,
  366. endTime: endDate.value ? endDate.value.format('YYYY-MM-DD') : filters.endTime
  367. }
  368. const data = await overviewLearningProgressApi.getStudyDetail(params)
  369. pagination.current = data.current
  370. pagination.total = data.total
  371. // 更新表格数据
  372. tableData.value = (data.records || []).map((item, index) => ({
  373. ...item,
  374. key: item.courseId || index + 1
  375. }))
  376. } catch (error) {
  377. console.error('获取学习明细数据失败:', error)
  378. message.error('获取学习明细数据失败')
  379. }
  380. }
  381. // 刷新所有数据
  382. const refreshData = async () => {
  383. loading.value = true
  384. try {
  385. // 更新筛选条件
  386. filters.startTime = startDate.value ? startDate.value.format('YYYY-MM-DD') : filters.startTime
  387. filters.endTime = endDate.value ? endDate.value.format('YYYY-MM-DD') : filters.endTime
  388. await Promise.all([fetchOverviewStats(), fetchTrendData(), fetchStudyDetail()])
  389. } catch (error) {
  390. console.error('刷新数据失败:', error)
  391. } finally {
  392. loading.value = false
  393. }
  394. }
  395. // 窗口大小变化处理
  396. const handleResize = () => {
  397. if (lineChart) lineChart.resize()
  398. if (barChart) barChart.resize()
  399. }
  400. const getCourseinfoAllList = () => {
  401. courseinfoAllList()
  402. .then((res) => {
  403. courseinfoAllListOptions.value = res.data
  404. })
  405. .catch((err) => {
  406. console.log(err)
  407. })
  408. }
  409. // 监听筛选条件变化
  410. watch(
  411. () => [filters.courseId, startDate.value, endDate.value],
  412. () => {
  413. refreshData()
  414. },
  415. { deep: true }
  416. )
  417. // 生命周期
  418. onMounted(async () => {
  419. await nextTick()
  420. initLineChart()
  421. initBarChart()
  422. window.addEventListener('resize', handleResize)
  423. // 初始化数据
  424. await refreshData()
  425. getCourseinfoAllList()
  426. })
  427. onUnmounted(() => {
  428. if (lineChart) {
  429. lineChart.dispose()
  430. lineChart = null
  431. }
  432. if (barChart) {
  433. barChart.dispose()
  434. barChart = null
  435. }
  436. window.removeEventListener('resize', handleResize)
  437. })
  438. </script>
  439. <style scoped>
  440. .card-header {
  441. border-left: 4px solid;
  442. }
  443. .chart-container {
  444. background-color: white;
  445. border: 1px solid #e4e7ed;
  446. border-radius: 4px;
  447. }
  448. .font-size-30 {
  449. font-size: 30px;
  450. }
  451. </style>