courseStatistics.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. <template>
  2. <div class="course-statistics-container">
  3. <!-- 筛选条件 -->
  4. <el-form :inline="true" :model="queryParams" class="demo-form-inline" style="margin-bottom: 20px;">
  5. <el-form-item label="小节名称">
  6. <el-input
  7. v-model="queryParams.videoName"
  8. placeholder="请输入小节名称关键字"
  9. clearable
  10. size="small"
  11. style="width: 300px;"
  12. @keyup.enter.native="handleQuery"
  13. />
  14. </el-form-item>
  15. <el-form-item>
  16. <el-button type="primary" size="small" @click="handleQuery">查询</el-button>
  17. <el-button size="small" @click="resetQuery">重置</el-button>
  18. </el-form-item>
  19. </el-form>
  20. <!-- 数据表格 -->
  21. <el-table
  22. v-loading="loading"
  23. :data="list"
  24. border
  25. style="width: 100%"
  26. >
  27. <el-table-column label="课程名称" align="center" prop="courseName" width="200" />
  28. <el-table-column label="小节" align="center" prop="videoName" width="210" />
  29. <el-table-column label="开课状态" align="center" prop="openStatus" width="120">
  30. <template slot-scope="scope">
  31. <el-tag :type="scope.row.openStatus === '已开课' ? 'success' : scope.row.openStatus === '已结束' ? 'info' : 'warning'">
  32. {{ scope.row.openStatus }}
  33. </el-tag>
  34. </template>
  35. </el-table-column>
  36. <el-table-column label="营期时间" align="center" prop="periodDate" width="120" />
  37. <el-table-column label="开始时间" align="center" prop="startTime" width="180" />
  38. <el-table-column label="结束时间" align="center" prop="endTime" width="180" />
  39. <el-table-column label="操作" align="center" fixed="right">
  40. <template slot-scope="scope">
  41. <el-button
  42. type="text"
  43. size="small"
  44. @click="handleViewDetail(scope.row)"
  45. >
  46. 查看详情
  47. </el-button>
  48. </template>
  49. </el-table-column>
  50. </el-table>
  51. <!-- 分页 -->
  52. <pagination
  53. v-show="total > 0"
  54. :total="total"
  55. :page.sync="queryParams.pageNum"
  56. :limit.sync="queryParams.pageSize"
  57. @pagination="getList"
  58. />
  59. <!-- 课程详情抽屉 -->
  60. <el-drawer
  61. title="课程小结详情"
  62. :visible.sync="detailDialog.visible"
  63. direction="rtl"
  64. size="60%"
  65. :close-on-click-modal="false"
  66. append-to-body
  67. class="course-detail-drawer"
  68. >
  69. <div v-loading="detailDialog.loading" style="padding: 20px;">
  70. <!-- 第一块:总体数据 -->
  71. <el-card class="detail-card" shadow="never">
  72. <div slot="header" class="card-header">
  73. <span>总体数据</span>
  74. <el-button type="primary" size="small" @click="handleViewUserDetail">查看用户详情</el-button>
  75. </div>
  76. <el-row :gutter="20">
  77. <el-col :span="6">
  78. <div class="stat-item">
  79. <div class="stat-label">视频时长</div>
  80. <div class="stat-value">{{ formatDuration(detailDialog.data.videoDuration) }}</div>
  81. </div>
  82. </el-col>
  83. <el-col :span="6">
  84. <div class="stat-item">
  85. <div class="stat-label">累计观看人数</div>
  86. <div class="stat-value">{{ detailDialog.data.totalWatchCount || 0 }}</div>
  87. </div>
  88. </el-col>
  89. <el-col :span="6">
  90. <div class="stat-item">
  91. <div class="stat-label">累计完课人数</div>
  92. <div class="stat-value">{{ detailDialog.data.totalCompleteCount || 0 }}</div>
  93. </div>
  94. </el-col>
  95. <el-col :span="6">
  96. <div class="stat-item">
  97. <div class="stat-label">到课完课率</div>
  98. <div class="stat-value">{{ detailDialog.data.completeRate || '0%' }}</div>
  99. </div>
  100. </el-col>
  101. </el-row>
  102. </el-card>
  103. <!-- 第二块:首次点播数据 -->
  104. <!-- <el-card class="detail-card" shadow="never">-->
  105. <!-- <div slot="header" class="card-header">-->
  106. <!-- <span>首次点播数据</span>-->
  107. <!-- </div>-->
  108. <!-- <el-row :gutter="20">-->
  109. <!-- <el-col :span="6">-->
  110. <!-- <div class="stat-item">-->
  111. <!-- <div class="stat-label">观看人数</div>-->
  112. <!-- <div class="stat-value">{{ detailDialog.data.firstWatchCount || 0 }}</div>-->
  113. <!-- </div>-->
  114. <!-- </el-col>-->
  115. <!-- <el-col :span="6">-->
  116. <!-- <div class="stat-item">-->
  117. <!-- <div class="stat-label">>=20分钟人数(首次)</div>-->
  118. <!-- <div class="stat-value">{{ detailDialog.data.firstWatch20MinCount || 0 }}</div>-->
  119. <!-- </div>-->
  120. <!-- </el-col>-->
  121. <!-- <el-col :span="6">-->
  122. <!-- <div class="stat-item">-->
  123. <!-- <div class="stat-label">>=30分钟人数(首次)</div>-->
  124. <!-- <div class="stat-value">{{ detailDialog.data.firstWatch30MinCount || 0 }}</div>-->
  125. <!-- </div>-->
  126. <!-- </el-col>-->
  127. <!-- <el-col :span="6">-->
  128. <!-- <div class="stat-item">-->
  129. <!-- <div class="stat-label">到课完课率首次(>=20分钟)</div>-->
  130. <!-- <div class="stat-value">{{ detailDialog.data.firstCompleteRate20Min || '0%' }}</div>-->
  131. <!-- </div>-->
  132. <!-- </el-col>-->
  133. <!-- <el-col :span="6">-->
  134. <!-- <div class="stat-item">-->
  135. <!-- <div class="stat-label">到课完课率首次(>=30分钟)</div>-->
  136. <!-- <div class="stat-value">{{ detailDialog.data.firstCompleteRate30Min || '0%' }}</div>-->
  137. <!-- </div>-->
  138. <!-- </el-col>-->
  139. <!-- </el-row>-->
  140. <!-- </el-card>-->
  141. <!-- 第三块:实际看课数据(修复后) -->
  142. <el-card class="detail-card" shadow="never">
  143. <div slot="header" class="card-header">
  144. <span>实际看课数据</span>
  145. </div>
  146. <el-row :gutter="20">
  147. <el-col :span="6">
  148. <div class="stat-item">
  149. <div class="stat-label">实际到课人数</div>
  150. <div class="stat-value">{{ detailDialog.data.totalStudents || 0 }}</div>
  151. </div>
  152. </el-col>
  153. <el-col :span="6">
  154. <div class="stat-item">
  155. <div class="stat-label">实际完课人数</div>
  156. <div class="stat-value">{{ detailDialog.data.completedCount || 0 }}</div>
  157. </div>
  158. </el-col>
  159. <el-col :span="6">
  160. <div class="stat-item">
  161. <div class="stat-label">实际完课率</div>
  162. <div class="stat-value">{{ detailDialog.data.actualCompletionRate ? detailDialog.data.actualCompletionRate + '%' : '0%' }}</div>
  163. </div>
  164. </el-col>
  165. <el-col :span="6">
  166. <div class="stat-item">
  167. <div class="stat-label">人均看课时长</div>
  168. <div class="stat-value">{{ formatDuration(detailDialog.data.avgWatchDurationMinutes)}}</div>
  169. </div>
  170. </el-col>
  171. <el-col :span="6">
  172. <div class="stat-item">
  173. <div class="stat-label">人均完课时长</div>
  174. <div class="stat-value">{{ formatDuration(detailDialog.data.avgCompletedDuration)}}</div>
  175. </div>
  176. </el-col>
  177. <el-col :span="6">
  178. <div class="stat-item">
  179. <div class="stat-label">人均完课完播率</div>
  180. <div class="stat-value">{{ detailDialog.data.avgCompletionPlaybackRate ? detailDialog.data.avgCompletionPlaybackRate + '%' : '0%' }}</div>
  181. </div>
  182. </el-col>
  183. </el-row>
  184. </el-card>
  185. <!-- 第四块:订单数据 -->
  186. <el-card class="detail-card" shadow="never">
  187. <div slot="header" class="card-header">
  188. <span>订单数据</span>
  189. </div>
  190. <el-row :gutter="20">
  191. <el-col :span="6">
  192. <div class="stat-item">
  193. <div class="stat-label">GMV</div>
  194. <div class="stat-value">¥{{ detailDialog.data.gmv || '0.00' }}</div>
  195. </div>
  196. </el-col>
  197. <el-col :span="6">
  198. <div class="stat-item">
  199. <div class="stat-label">付费人数</div>
  200. <div class="stat-value">{{ detailDialog.data.paidUserCount || 0 }}</div>
  201. </div>
  202. </el-col>
  203. <el-col :span="6">
  204. <div class="stat-item">
  205. <div class="stat-label">付费单数</div>
  206. <div class="stat-value">{{ detailDialog.data.paidOrderCount || 0 }}</div>
  207. </div>
  208. </el-col>
  209. <el-col :span="6">
  210. <div class="stat-item">
  211. <div class="stat-label">总付费转换率</div>
  212. <div class="stat-value">{{ detailDialog.data.totalPaidConversionRate || '0%' }}</div>
  213. </div>
  214. </el-col>
  215. <el-col :span="6">
  216. <div class="stat-item">
  217. <div class="stat-label">20min付费转化率</div>
  218. <div class="stat-value">{{ detailDialog.data.paidConversionRate20Min || '0%' }}</div>
  219. </div>
  220. </el-col>
  221. <el-col :span="6">
  222. <div class="stat-item">
  223. <div class="stat-label">完课R值</div>
  224. <div class="stat-value">{{ detailDialog.data.completeRValue || '0.00' }}</div>
  225. </div>
  226. </el-col>
  227. <el-col :span="6">
  228. <div class="stat-item">
  229. <div class="stat-label">领红包人数</div>
  230. <div class="stat-value">{{ detailDialog.data.redPacketUserCount || 0 }}</div>
  231. </div>
  232. </el-col>
  233. <el-col :span="6">
  234. <div class="stat-item">
  235. <div class="stat-label">答题人数</div>
  236. <div class="stat-value">{{ detailDialog.data.answerUserCount || 0 }}</div>
  237. </div>
  238. </el-col>
  239. </el-row>
  240. </el-card>
  241. <!-- 第五块:单品销量统计 -->
  242. <el-card class="detail-card" shadow="never">
  243. <div slot="header" class="card-header">
  244. <span>商品销量统计</span>
  245. </div>
  246. <el-table
  247. :data="detailDialog.data.productList || []"
  248. border
  249. style="width: 100%"
  250. >
  251. <el-table-column label="商品名称" align="center" prop="productName" />
  252. <el-table-column label="销量" align="center" prop="salesCount" />
  253. <el-table-column label="销售额" align="center" prop="salesAmount">
  254. <template slot-scope="scope">
  255. ¥{{ scope.row.salesAmount || '0.00' }}
  256. </template>
  257. </el-table-column>
  258. </el-table>
  259. </el-card>
  260. </div>
  261. </el-drawer>
  262. <!-- 用户详情抽屉 -->
  263. <el-drawer
  264. title="用户看课数据"
  265. :visible.sync="userDetailDialog.visible"
  266. direction="rtl"
  267. size="60%"
  268. :close-on-click-modal="false"
  269. append-to-body
  270. >
  271. <div v-loading="userDetailDialog.loading" style="padding: 20px;">
  272. <div style="margin-bottom: 20px; text-align: right;">
  273. <el-button type="primary" size="small" @click="handleExportUserDetail">导出用户详情</el-button>
  274. </div>
  275. <el-table
  276. :data="userDetailDialog.list"
  277. border
  278. style="width: 100%"
  279. >
  280. <el-table-column label="用户名称" align="center" prop="userName" width="150" />
  281. <!-- <el-table-column label="观看时长" align="center" prop="watchDuration" width="120">-->
  282. <!-- <template slot-scope="scope">-->
  283. <!-- {{ formatDuration(scope.row.watchDuration) }}-->
  284. <!-- </template>-->
  285. <!-- </el-table-column>-->
  286. <el-table-column label="观看时长" align="center" prop="repeatWatchDuration" width="150">
  287. <template slot-scope="scope">
  288. {{ formatDuration(scope.row.repeatWatchDuration) }}
  289. </template>
  290. </el-table-column>
  291. <el-table-column label="订单数" align="center" prop="orderCount" width="100" />
  292. <el-table-column label="订单金额" align="center" prop="orderAmount" width="120">
  293. <template slot-scope="scope">
  294. ¥{{ scope.row.orderAmount || '0.00' }}
  295. </template>
  296. </el-table-column>
  297. <el-table-column label="分公司名称" align="center" prop="companyName" width="150" />
  298. <el-table-column label="销售名称" align="center" prop="salesName" />
  299. </el-table>
  300. <!-- 分页 -->
  301. <pagination
  302. v-show="userDetailDialog.total > 0"
  303. :total="userDetailDialog.total"
  304. :page.sync="userDetailDialog.queryParams.pageNum"
  305. :limit.sync="userDetailDialog.queryParams.pageSize"
  306. @pagination="getUserDetailList"
  307. />
  308. </div>
  309. </el-drawer>
  310. </div>
  311. </template>
  312. <script>
  313. import { getDays } from "@/api/course/userCoursePeriod";
  314. import { getCourseStatisticsDetail, getCourseStatisticsUserDetailList, exportCourseStatisticsUserDetail } from "@/api/course/courseWatchLog";
  315. import { download } from "@/utils/common";
  316. export default {
  317. name: "CourseStatistics",
  318. props: {
  319. periodId: {
  320. type: [String, Number],
  321. default: null
  322. },
  323. active: {
  324. type: Boolean,
  325. default: false
  326. }
  327. },
  328. data() {
  329. return {
  330. loading: false,
  331. list: [],
  332. total: 0,
  333. queryParams: {
  334. pageNum: 1,
  335. pageSize: 10,
  336. videoName: null,
  337. periodId: null
  338. },
  339. // 详情弹窗
  340. detailDialog: {
  341. visible: false,
  342. loading: false,
  343. data: {}
  344. },
  345. // 用户详情弹窗
  346. userDetailDialog: {
  347. visible: false,
  348. loading: false,
  349. list: [],
  350. total: 0,
  351. queryParams: {
  352. pageNum: 1,
  353. pageSize: 10,
  354. videoId: null,
  355. periodId: null
  356. }
  357. }
  358. };
  359. },
  360. watch: {
  361. active(newVal) {
  362. if (newVal && this.periodId) {
  363. this.queryParams.periodId = this.periodId;
  364. this.getList();
  365. }
  366. },
  367. periodId(newVal) {
  368. if (newVal && this.active) {
  369. this.queryParams.periodId = newVal;
  370. this.getList();
  371. }
  372. }
  373. },
  374. mounted() {
  375. if (this.active && this.periodId) {
  376. this.queryParams.periodId = this.periodId;
  377. this.getList();
  378. }
  379. },
  380. methods: {
  381. /** 查询列表 */
  382. getList() {
  383. if (!this.queryParams.periodId) {
  384. this.loading = false;
  385. return;
  386. }
  387. this.loading = true;
  388. getDays(this.queryParams).then(response => {
  389. if (response.code === 200) {
  390. // 映射字段并格式化数据
  391. this.list = (response.rows || []).map(item => {
  392. return {
  393. ...item,
  394. // 映射字段
  395. periodDate: item.dayDate,
  396. startTime: item.startDateTime,
  397. endTime: item.endDateTime,
  398. // 格式化开课状态
  399. openStatus: this.formatCourseStatus(item.status),
  400. // 小节状态(可以根据需要设置,这里先使用开课状态)
  401. videoStatus: this.formatCourseStatus(item.status)
  402. };
  403. });
  404. this.total = response.total || 0;
  405. } else {
  406. this.$message.error(response.msg || '查询失败');
  407. this.list = [];
  408. this.total = 0;
  409. }
  410. this.loading = false;
  411. }).catch(error => {
  412. this.$message.error('查询失败:' + (error.message || '未知错误'));
  413. this.loading = false;
  414. this.list = [];
  415. this.total = 0;
  416. });
  417. },
  418. /** 格式化课程状态 */
  419. formatCourseStatus(status) {
  420. const statusMap = {
  421. 0: '未开始',
  422. 1: '已开课',
  423. 2: '已结束'
  424. };
  425. return statusMap[status] || '未知状态';
  426. },
  427. /** 搜索按钮操作 */
  428. handleQuery() {
  429. this.queryParams.pageNum = 1;
  430. this.getList();
  431. },
  432. /** 重置按钮操作 */
  433. resetQuery() {
  434. this.queryParams.videoName = null;
  435. this.queryParams.pageNum = 1;
  436. this.getList();
  437. },
  438. /** 查看详情 */
  439. handleViewDetail(row) {
  440. this.detailDialog.visible = true;
  441. this.detailDialog.loading = true;
  442. const videoId = row.videoId || row.id;
  443. const periodId = this.queryParams.periodId;
  444. if (!videoId || !periodId) {
  445. this.$message.error('视频ID或营期ID不能为空');
  446. this.detailDialog.loading = false;
  447. return;
  448. }
  449. getCourseStatisticsDetail(videoId, periodId).then(response => {
  450. if (response.code === 200 && response.data) {
  451. const data = response.data;
  452. // 安全获取实际看课数据对象
  453. const actualVO = data.fsActualCompletionVO || {};
  454. // 设置总体数据
  455. this.detailDialog.data = {
  456. // 总体数据
  457. videoDuration: data.videoDuration ?? 0,
  458. totalWatchCount: data.totalWatchCount ?? 0,
  459. totalCompleteCount: data.totalCompleteCount ?? 0,
  460. completeRate: data.completeRate != null ? Number(data.completeRate).toFixed(2) + '%' : '0%',
  461. // 首次点播数据
  462. firstWatchCount: data.firstWatchCount ?? 0,
  463. firstWatch20MinCount: data.firstWatch20MinCount ?? 0,
  464. firstWatch30MinCount: data.firstWatch30MinCount ?? 0,
  465. firstCompleteRate20Min: data.firstCompleteRate20Min != null ? Number(data.firstCompleteRate20Min).toFixed(2) + '%' : '0%',
  466. firstCompleteRate30Min: data.firstCompleteRate30Min != null ? Number(data.firstCompleteRate30Min).toFixed(2) + '%' : '0%',
  467. // 实际看课数据(修复后,所有字段都有默认值)
  468. totalStudents: actualVO.totalStudents ?? 0,
  469. completedCount: actualVO.completedCount ?? 0,
  470. actualCompletionRate: actualVO.actualCompletionRate != null ? Number(actualVO.actualCompletionRate).toFixed(2) : '0',
  471. avgWatchDurationMinutes: actualVO.avgWatchDurationMinutes ?? 0,
  472. avgCompletedDuration: actualVO.avgCompletedDuration ?? 0,
  473. avgCompletionPlaybackRate: actualVO.avgCompletionPlaybackRate != null ? Number(actualVO.avgCompletionPlaybackRate).toFixed(2) : '0',
  474. // 订单数据
  475. gmv: data.gmv != null ? Number(data.gmv).toFixed(2) : '0.00',
  476. paidUserCount: data.paidUserCount ?? 0,
  477. paidOrderCount: data.paidOrderCount ?? 0,
  478. totalPaidConversionRate: data.totalPaidConversionRate != null ? Number(data.totalPaidConversionRate).toFixed(2) + '%' : '0%',
  479. paidConversionRate20Min: data.paidConversionRate20Min != null ? Number(data.paidConversionRate20Min).toFixed(2) + '%' : '0%',
  480. completeRValue: data.completeRValue != null ? Number(data.completeRValue).toFixed(2) : '0.00',
  481. redPacketUserCount: data.redPacketUserCount ?? 0,
  482. answerUserCount: data.answerUserCount ?? 0,
  483. productList: (data.productList || []).map(p => ({
  484. productName: p.productName || '未知商品',
  485. salesCount: p.salesCount ?? 0,
  486. salesAmount: p.salesAmount != null ? Number(p.salesAmount).toFixed(2) : '0.00'
  487. })),
  488. videoId: videoId,
  489. id: row.id
  490. };
  491. } else {
  492. this.$message.error(response.msg || '获取数据失败');
  493. }
  494. this.detailDialog.loading = false;
  495. }).catch(error => {
  496. this.$message.error('获取数据失败:' + (error.message || '未知错误'));
  497. this.detailDialog.loading = false;
  498. });
  499. },
  500. /** 查看用户详情 */
  501. handleViewUserDetail() {
  502. this.userDetailDialog.visible = true;
  503. this.userDetailDialog.queryParams.videoId = this.detailDialog.data.videoId || this.detailDialog.data.id;
  504. this.userDetailDialog.queryParams.periodId = this.queryParams.periodId;
  505. this.userDetailDialog.queryParams.pageNum = 1;
  506. this.getUserDetailList();
  507. },
  508. /** 获取用户详情列表 */
  509. getUserDetailList() {
  510. const videoId = this.userDetailDialog.queryParams.videoId;
  511. const periodId = this.userDetailDialog.queryParams.periodId;
  512. if (!videoId || !periodId) {
  513. this.$message.error('视频ID或营期ID不能为空');
  514. this.userDetailDialog.loading = false;
  515. return;
  516. }
  517. this.userDetailDialog.loading = true;
  518. getCourseStatisticsUserDetailList(this.userDetailDialog.queryParams).then(response => {
  519. if (response.code === 200 && response.data) {
  520. const d = response.data;
  521. this.userDetailDialog.list = d.list || d.rows || [];
  522. this.userDetailDialog.total = d.total ?? 0;
  523. } else {
  524. this.userDetailDialog.list = [];
  525. this.userDetailDialog.total = 0;
  526. }
  527. this.userDetailDialog.loading = false;
  528. }).catch(() => {
  529. this.userDetailDialog.list = [];
  530. this.userDetailDialog.total = 0;
  531. this.userDetailDialog.loading = false;
  532. });
  533. },
  534. /** 导出用户详情(按创建时间倒序,最多50000条) */
  535. handleExportUserDetail() {
  536. const videoId = this.userDetailDialog.queryParams.videoId;
  537. const periodId = this.userDetailDialog.queryParams.periodId;
  538. if (!videoId || !periodId) {
  539. this.$message.error('请先查看用户详情');
  540. return;
  541. }
  542. this.$confirm('是否确认导出用户看课数据?(按创建时间倒序,最多50000条)', '提示', {
  543. confirmButtonText: '确定',
  544. cancelButtonText: '取消',
  545. type: 'warning'
  546. }).then(() => {
  547. exportCourseStatisticsUserDetail(videoId, periodId).then(response => {
  548. if (response.code === 200 && response.msg) {
  549. download(response.msg);
  550. this.$message.success('导出成功');
  551. } else {
  552. this.$message.error(response.msg || '导出失败');
  553. }
  554. }).catch(() => {
  555. this.$message.error('导出失败');
  556. });
  557. }).catch(() => {});
  558. },
  559. /** 格式化时长 */
  560. formatDuration(seconds) {
  561. if (seconds == null || isNaN(seconds)) return '0秒';
  562. let total = Math.abs(seconds);
  563. const hours = Math.floor(total / 3600);
  564. const minutes = Math.floor((total % 3600) / 60);
  565. const secs = Math.floor(total % 60);
  566. const parts = [];
  567. if (hours > 0) parts.push(`${hours}小时`);
  568. if (minutes > 0) parts.push(`${minutes}分`);
  569. if (secs > 0 || parts.length === 0) {
  570. parts.push(`${secs}秒`);
  571. }
  572. return parts.join('');
  573. }
  574. }
  575. };
  576. </script>
  577. <style scoped lang="scss">
  578. .course-statistics-container {
  579. padding: 20px;
  580. }
  581. .detail-card {
  582. margin-bottom: 20px;
  583. .card-header {
  584. display: flex;
  585. justify-content: space-between;
  586. align-items: center;
  587. font-weight: bold;
  588. font-size: 16px;
  589. }
  590. .stat-item {
  591. text-align: center;
  592. padding: 20px;
  593. border: 1px solid #EBEEF5;
  594. border-radius: 4px;
  595. background-color: #F5F7FA;
  596. margin-top: 10px;
  597. .stat-label {
  598. font-size: 14px;
  599. color: #606266;
  600. margin-bottom: 10px;
  601. }
  602. .stat-value {
  603. font-size: 24px;
  604. font-weight: bold;
  605. color: #303133;
  606. }
  607. }
  608. }
  609. .course-detail-drawer {
  610. ::v-deep .el-drawer__body {
  611. overflow-y: auto;
  612. }
  613. }
  614. </style>