statistics.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <template>
  2. <div class="statistics-container">
  3. <!-- 营期课程选择 -->
  4. <div class="fixed-header">
  5. <el-form :inline="true" :model="queryParams" class="demo-form-inline">
  6. <el-form-item label="营期课程">
  7. <el-select
  8. v-model="queryParams.videoIdList"
  9. multiple
  10. filterable
  11. remote
  12. :remote-method="handleCourseSearch"
  13. :loading="courseLoading"
  14. placeholder="请输入关键词搜索营期课程"
  15. style="width: 400px"
  16. @visible-change="handleCourseDropdownVisible"
  17. >
  18. <el-option
  19. v-for="item in courseOptions"
  20. :key="item.videoId"
  21. :label="item.videoName"
  22. :value="item.videoId"
  23. />
  24. <!-- 分页加载更多选项 -->
  25. <el-option v-if="hasMoreCourses" key="load-more" disabled class="load-more-option">
  26. <div class="load-more" @click="loadMoreCourses">
  27. <span>加载更多</span>
  28. <i class="el-icon-loading" v-if="loadingMore"></i>
  29. </div>
  30. </el-option>
  31. </el-select>
  32. </el-form-item>
  33. <el-form-item label="公司">
  34. <el-select
  35. v-model="queryParams.companyId"
  36. placeholder="请选择公司"
  37. clearable
  38. style="width: 400px"
  39. >
  40. <el-option
  41. v-for="item in companyOptions"
  42. :key="item.companyId"
  43. :label="item.companyName"
  44. :value="item.companyId"
  45. />
  46. </el-select>
  47. </el-form-item>
  48. <el-form-item>
  49. <el-button type="primary" @click="handleQuery">查询</el-button>
  50. <el-button type="info" :loading="exportLoading" @click="exportHandle">导出</el-button>
  51. </el-form-item>
  52. </el-form>
  53. <!-- 统计数据展示 -->
  54. <el-row :gutter="20" class="statistics-row">
  55. <el-col :span="3">
  56. <div class="statistics-item">
  57. <div class="statistics-title">完播人数</div>
  58. <div class="statistics-value">{{ statistics.courseCompleteNum || 0 }}</div>
  59. </div>
  60. </el-col>
  61. <el-col :span="3">
  62. <div class="statistics-item">
  63. <div class="statistics-title">观看人数</div>
  64. <div class="statistics-value">{{ statistics.courseWatchNum || 0 }}</div>
  65. </div>
  66. </el-col>
  67. <el-col :span="3">
  68. <div class="statistics-item">
  69. <div class="statistics-title">完播率</div>
  70. <div class="statistics-value">{{ statistics.completeRate || '0%' }}</div>
  71. </div>
  72. </el-col>
  73. <el-col :span="3">
  74. <div class="statistics-item">
  75. <div class="statistics-title">观看总次数</div>
  76. <div class="statistics-value">{{ statistics.courseWatchTimes || 0 }}</div>
  77. </div>
  78. </el-col>
  79. <el-col :span="3">
  80. <div class="statistics-item">
  81. <div class="statistics-title">答题总次数</div>
  82. <div class="statistics-value">{{ statistics.answerTimes || 0 }}</div>
  83. </div>
  84. </el-col>
  85. <el-col :span="3">
  86. <div class="statistics-item">
  87. <div class="statistics-title">答题正确总次数</div>
  88. <div class="statistics-value">{{ statistics.answerRightTimes || 0 }}</div>
  89. </div>
  90. </el-col>
  91. <el-col :span="3">
  92. <div class="statistics-item">
  93. <div class="statistics-title">奖励金额总计(元)</div>
  94. <div class="statistics-value">{{ statistics.redPacketAmount || 0 }}</div>
  95. </div>
  96. </el-col>
  97. </el-row>
  98. </div>
  99. <!-- 列表统计展示 -->
  100. <div class="table-wrapper">
  101. <el-table v-loading="loading" :data="list" border height="calc(100vh - 450px)">
  102. <el-table-column type="index" label="序号" width="50" align="center" fixed/>
  103. <el-table-column prop="title" label="课程名称" align="center" min-width="250" fixed/>
  104. <el-table-column prop="dayDate" label="营期日期" align="center" min-width="120" fixed/>
  105. <el-table-column prop="countDetailsVO.courseWatchTimes" label="观看次数" align="center" min-width="100"/>
  106. <el-table-column prop="countDetailsVO.courseCompleteTimes" label="完播次数" align="center" min-width="100"/>
  107. <el-table-column prop="countDetailsVO.courseWatchNum" label="观看人数" align="center" min-width="100"/>
  108. <el-table-column prop="countDetailsVO.courseCompleteNum" label="完播人数" align="center" min-width="100"/>
  109. <el-table-column prop="countDetailsVO.completeRate" label="完播率" align="center" min-width="100">
  110. <template slot-scope="scope">
  111. {{ scope.row.countDetailsVO.completeRate || 0 }}%
  112. </template>
  113. </el-table-column>
  114. <el-table-column prop="countDetailsVO.answerTimes" label="答题次数" align="center" min-width="100"/>
  115. <el-table-column prop="countDetailsVO.answerNum" label="答题人数" align="center" min-width="100"/>
  116. <el-table-column prop="countDetailsVO.answerRightNum" label="正确人数" align="center" min-width="100"/>
  117. <el-table-column prop="countDetailsVO.answerRightRate" label="正确率" align="center" min-width="100">
  118. <template slot-scope="scope">
  119. {{ scope.row.countDetailsVO.answerRightRate || 0 }}%
  120. </template>
  121. </el-table-column>
  122. <el-table-column prop="countDetailsVO.redPacketNum" label="答题红包个数" align="center" min-width="120"/>
  123. <el-table-column prop="countDetailsVO.redPacketAmount" label="答题红包金额(元)" align="center" min-width="150"/>
  124. </el-table>
  125. <!-- 分页 -->
  126. <div class="custom-pagination-container">
  127. <pagination
  128. v-show="total > 0"
  129. :total="total"
  130. :page.sync="queryParams.pageNum"
  131. :limit.sync="queryParams.pageSize"
  132. @pagination="getCountList"
  133. />
  134. </div>
  135. </div>
  136. </div>
  137. </template>
  138. <script>
  139. import {getDays, periodCountSelect, getPeriodCompanyList,exportInfo} from "@/api/course/userCoursePeriod";
  140. export default {
  141. name: "CourseStatistics",
  142. props: {
  143. periodId: {
  144. type: [String, Number],
  145. default: ''
  146. },
  147. active: {
  148. type: Boolean,
  149. default: false
  150. }
  151. },
  152. data() {
  153. return {
  154. exportLoading: false,
  155. // 遮罩层
  156. loading: false,
  157. courseLoading: false,
  158. loadingMore: false,
  159. // 总条数
  160. total: 0,
  161. // 课程选项
  162. courseOptions: [],
  163. courseTotal: 0,
  164. courseQueryParams: {
  165. pageNum: 1,
  166. pageSize: 10, // 默认每页10条
  167. videoName: '',
  168. periodId: ''
  169. },
  170. hasMoreCourses: false,
  171. companyOptions: [],
  172. // 统计数据
  173. statistics: {
  174. courseCompleteNum: 0,
  175. courseWatchNum: 0,
  176. completeRate: '0%',
  177. courseWatchTimes: 0,
  178. answerTimes: 0,
  179. answerRightTimes: 0,
  180. redPacketAmount: 0
  181. },
  182. // 列表数据
  183. list: [],
  184. // 查询参数
  185. queryParams: {
  186. pageNum: 1,
  187. pageSize: 100,
  188. videoIdList: [],
  189. companyId: '',
  190. periodId: ''
  191. },
  192. // 是否已初始化
  193. initialized: false
  194. };
  195. },
  196. watch: {
  197. periodId: {
  198. handler(newVal) {
  199. this.queryParams.periodId = newVal;
  200. this.courseQueryParams.periodId = newVal;
  201. if (this.active && !this.initialized) {
  202. this.initializeData();
  203. }
  204. },
  205. immediate: true
  206. },
  207. active: {
  208. handler(newVal) {
  209. if (newVal && !this.initialized) {
  210. this.initializeData();
  211. }
  212. },
  213. immediate: true
  214. }
  215. },
  216. methods: {
  217. /** 初始化数据 */
  218. initializeData() {
  219. this.getCourseOptions();
  220. this.getCompanyOptions();
  221. this.initialized = true;
  222. },
  223. getCompanyOptions() {
  224. getPeriodCompanyList({
  225. periodId: this.periodId
  226. }).then(response => {
  227. this.companyOptions = response.data || [];
  228. });
  229. },
  230. /** 获取课程选项 */
  231. getCourseOptions(isLoadMore = false) {
  232. if (!isLoadMore) {
  233. this.courseLoading = true;
  234. this.courseQueryParams.pageNum = 1; // 重置页码
  235. } else {
  236. this.loadingMore = true;
  237. }
  238. getDays(this.courseQueryParams).then(r => {
  239. if (r.code === 200) {
  240. if (isLoadMore) {
  241. // 加载更多时,追加数据
  242. this.courseOptions = [...this.courseOptions, ...r.rows];
  243. } else {
  244. // 首次加载或搜索时,替换数据
  245. this.courseOptions = r.rows;
  246. }
  247. this.courseTotal = r.total || 0;
  248. // 计算是否还有更多数据
  249. const loadedCount = this.courseOptions.length;
  250. this.hasMoreCourses = loadedCount < this.courseTotal;
  251. this.courseLoading = false;
  252. this.loadingMore = false;
  253. } else {
  254. this.$message.error(r.msg || '获取数据失败');
  255. this.courseLoading = false;
  256. this.loadingMore = false;
  257. }
  258. }).catch(() => {
  259. this.courseLoading = false;
  260. this.loadingMore = false;
  261. });
  262. },
  263. /** 加载更多课程 */
  264. loadMoreCourses() {
  265. if (this.loadingMore) return;
  266. this.courseQueryParams.pageNum += 1;
  267. this.getCourseOptions(true);
  268. },
  269. /** 处理下拉框显示/隐藏 */
  270. handleCourseDropdownVisible(visible) {
  271. if (visible && this.courseOptions.length === 0) {
  272. // 当下拉框显示且没有数据时,加载初始数据
  273. this.getCourseOptions();
  274. }
  275. },
  276. /** 营期课程搜索 */
  277. handleCourseSearch(query) {
  278. this.courseQueryParams.videoName = query;
  279. this.courseQueryParams.pageNum = 1; // 搜索时重置页码
  280. this.getCourseOptions();
  281. },
  282. /** 查询按钮操作 */
  283. handleQuery() {
  284. this.queryParams.pageNum = 1;
  285. this.getCountList();
  286. },
  287. /** 导出 */
  288. exportHandle(){
  289. this.$confirm('是否确认导出所有课程统计信息数据项?', "警告", {
  290. confirmButtonText: "确定",
  291. cancelButtonText: "取消",
  292. type: "warning"
  293. }).then(() => {
  294. this.exportLoading = true;
  295. return exportInfo(this.queryParams);
  296. }).then(response => {
  297. this.download(response.msg);
  298. this.exportLoading = false;
  299. }).catch(() => {});
  300. },
  301. /** 获取列表数据 */
  302. getCountList() {
  303. this.loading = true;
  304. periodCountSelect(this.queryParams).then(response => {
  305. if (response.code === 200) {
  306. // 设置列表数据
  307. this.list = response.rows;
  308. this.total = response.total || 0;
  309. // 计算总统计数据
  310. this.calculateTotalStatistics();
  311. console.log('列表数据:', this.list);
  312. } else {
  313. this.$message.error(response.msg || '获取数据失败');
  314. }
  315. this.loading = false;
  316. }).catch(error => {
  317. console.error('获取数据失败:', error);
  318. this.$message.error('获取数据失败');
  319. this.loading = false;
  320. });
  321. },
  322. /** 计算总统计数据 */
  323. calculateTotalStatistics() {
  324. // 初始化统计数据
  325. this.statistics = {
  326. courseCompleteNum: 0,
  327. courseWatchNum: 0,
  328. completeRate: '0%',
  329. courseWatchTimes: 0,
  330. answerTimes: 0,
  331. answerRightTimes: 0,
  332. redPacketAmount: 0
  333. };
  334. // 如果没有数据,直接返回
  335. if (!this.list || this.list.length === 0) {
  336. return;
  337. }
  338. // 遍历列表数据,累加各项统计数据
  339. this.list.forEach(item => {
  340. const details = item.countDetailsVO || {};
  341. // 累加各项数据
  342. this.statistics.courseCompleteNum += details.courseCompleteNum || 0;
  343. this.statistics.courseWatchNum += details.courseWatchNum || 0;
  344. this.statistics.courseWatchTimes += details.courseWatchTimes || 0;
  345. this.statistics.answerTimes += details.answerTimes || 0;
  346. this.statistics.answerRightTimes += details.answerRightNum || 0;
  347. this.statistics.redPacketAmount += details.redPacketAmount || 0;
  348. });
  349. // 计算完播率
  350. if (this.statistics.courseWatchNum > 0) {
  351. const rate = (this.statistics.courseCompleteNum / this.statistics.courseWatchNum * 100).toFixed(2);
  352. this.statistics.completeRate = rate + '%';
  353. }
  354. }
  355. }
  356. };
  357. </script>
  358. <style scoped>
  359. .statistics-container {
  360. height: 100%;
  361. overflow: hidden;
  362. position: relative;
  363. }
  364. .fixed-header {
  365. position: sticky;
  366. top: 0;
  367. z-index: 10;
  368. background-color: #fff;
  369. padding: 10px 0;
  370. border-bottom: 1px solid #EBEEF5;
  371. }
  372. .table-wrapper {
  373. height: calc(100% - 220px);
  374. overflow: visible;
  375. position: relative;
  376. }
  377. .custom-pagination-container {
  378. padding: 10px 0 12px 0;
  379. text-align: right;
  380. background-color: #fff;
  381. position: relative;
  382. z-index: 1;
  383. }
  384. /* 覆盖原有的pagination-container样式 */
  385. :deep(.pagination-container) {
  386. height: auto !important;
  387. margin-bottom: 0 !important;
  388. margin-top: 0 !important;
  389. padding: 0 !important;
  390. }
  391. .statistics-row {
  392. margin: 20px 0;
  393. }
  394. .statistics-item {
  395. background-color: #f5f7fa;
  396. border-radius: 4px;
  397. padding: 15px;
  398. text-align: center;
  399. height: 100px;
  400. display: flex;
  401. flex-direction: column;
  402. justify-content: center;
  403. }
  404. .statistics-title {
  405. font-size: 14px;
  406. color: #606266;
  407. margin-bottom: 10px;
  408. }
  409. .statistics-value {
  410. font-size: 20px;
  411. font-weight: bold;
  412. color: #303133;
  413. }
  414. /* 加载更多选项样式 */
  415. .load-more-option {
  416. text-align: center;
  417. padding: 8px 0;
  418. }
  419. .load-more {
  420. color: #409EFF;
  421. cursor: pointer;
  422. display: flex;
  423. align-items: center;
  424. justify-content: center;
  425. gap: 5px;
  426. }
  427. .load-more:hover {
  428. color: #66b1ff;
  429. }
  430. :deep(.el-select-dropdown__list) {
  431. padding-bottom: 0 !important;
  432. }
  433. </style>