courseReport.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. <template>
  2. <div class="app-container">
  3. <!-- 添加维度切换Tab -->
  4. <el-tabs v-model="activeDimension" @tab-click="handleDimensionChange">
  5. <el-tab-pane label="公司维度" name="company"></el-tab-pane>
  6. <el-tab-pane label="课程维度" name="course"></el-tab-pane>
  7. <el-tab-pane label="小节维度" name="video"></el-tab-pane>
  8. </el-tabs>
  9. <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="85px">
  10. <el-form-item label="公司名" prop="companyId" v-if="activeDimension === 'company'">
  11. <el-select filterable v-model="queryParams.companyId" placeholder="请选择公司名"
  12. clearable size="small">
  13. <el-option
  14. v-for="item in companys"
  15. :key="item.companyId"
  16. :label="item.companyName"
  17. :value="item.companyId"
  18. />
  19. </el-select>
  20. </el-form-item>
  21. <el-form-item label="课程" prop="courseId" v-if="activeDimension === 'course'||activeDimension === 'video'">
  22. <el-select filterable v-model="queryParams.courseId" placeholder="请选择课程"
  23. clearable size="small" @change="handleCourseChange">
  24. <el-option
  25. v-for="item in courses"
  26. :key="item.courseId"
  27. :label="item.dictLabel"
  28. :value="item.dictValue"
  29. />
  30. </el-select>
  31. </el-form-item>
  32. <el-form-item label="小节" prop="videoId" v-if="activeDimension === 'video' && queryParams.courseId">
  33. <el-select filterable v-model="queryParams.videoId" placeholder="请选择小节"
  34. clearable size="small">
  35. <el-option
  36. v-for="item in videos"
  37. :key="item.videoId"
  38. :label="item.dictLabel"
  39. :value="item.dictValue"
  40. />
  41. </el-select>
  42. </el-form-item>
  43. <el-form-item>
  44. <el-form-item label="看课时间" prop="createTime">
  45. <el-date-picker v-model="createTime" size="small" style="width: 220px" value-format="yyyy-MM-dd"
  46. type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
  47. @change="xdChange"></el-date-picker>
  48. </el-form-item>
  49. <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  50. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  51. </el-form-item>
  52. </el-form>
  53. <el-row :gutter="10" class="mb8">
  54. <el-col :span="1.5">
  55. <el-button
  56. type="warning"
  57. plain
  58. icon="el-icon-download"
  59. size="mini"
  60. :loading="exportLoading"
  61. @click="handleExport"
  62. >导出</el-button>
  63. </el-col>
  64. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  65. </el-row>
  66. <el-table height="500" v-loading="loading" border :data="packageOrderList">
  67. <el-table-column label="销售公司" align="center" prop="companyName" width="120px"
  68. v-if="activeDimension === 'company'"/>
  69. <el-table-column label="课程名称" align="center" prop="courseName"
  70. v-if="activeDimension === 'course'"/>
  71. <el-table-column label="小节名称" align="center" prop="videoName"
  72. v-if="activeDimension === 'video'"/>
  73. <el-table-column label="进线人数" align="center" prop="accessCount"/>
  74. <el-table-column label="完课人数" align="center" prop="finishedCount"/>
  75. <el-table-column label="完播数" align="center" prop="courseCompleteTimes"/>
  76. <el-table-column label="完课率" align="center" prop="finishRate"/>
  77. </el-table>
  78. <div class="total-summary">
  79. <span class="total-title">总计:</span>
  80. <span class="total-item">进线人数: {{ calculatedTotalData.accessCount }}</span>
  81. <span class="total-item">完课人数: {{ calculatedTotalData.finishedCount }}</span>
  82. <span class="total-item">完课率: {{ calculatedTotalData.finishRate}}</span>
  83. <span class="total-item">完播数: {{ calculatedTotalData.courseCompleteTimes }}</span>
  84. </div>
  85. <pagination
  86. v-show="total>0"
  87. :total="total"
  88. :page.sync="queryParams.pageNum"
  89. :limit.sync="queryParams.pageSize"
  90. @pagination="getList"
  91. />
  92. </div>
  93. </template>
  94. <script>
  95. import {
  96. listPackageOrder, getPackageOrder, delPackageOrder, addPackageOrder, updatePackageOrder, exportPackageOrder,
  97. PackageOrderReport, orderReport, courseReport, exportOrderReport, exportCourseReport
  98. } from "@/api/his/packageOrder";
  99. import {getCompanyList} from "@/api/company/company";
  100. import packageOrderDetails from '../../components/his/packageOrderDetails.vue';
  101. import {treeselect} from "@/api/company/companyDept";
  102. import Treeselect from "@riophae/vue-treeselect";
  103. import "@riophae/vue-treeselect/dist/vue-treeselect.css";
  104. import {getTask} from "@/api/common";
  105. import {getCourseList, getVideosByCourse} from "@/api/course/userWatchCourseStatistics";
  106. export default {
  107. name: "PackageOrder",
  108. components: {packageOrderDetails, Treeselect},
  109. data() {
  110. return {
  111. normalizer: function(node) {
  112. return {
  113. id: node.id || node.dictValue,
  114. label: node.label || node.dictLabel,
  115. children: node.children
  116. }
  117. },
  118. // 添加用于存储计算总和的数据
  119. calculatedTotalData: {
  120. accessCount: 0,
  121. finishedCount: 0,
  122. finishRate: '0%',
  123. courseCompleteTimes: 0,
  124. },
  125. totalData: {},
  126. companys: [],
  127. courses: [], // 课程列表
  128. videos: [], // 小节列表
  129. activeDimension: 'company', // 当前激活的维度
  130. companyId: undefined,
  131. show: {
  132. open: false,
  133. },
  134. sourceOptions: [],
  135. actName: "2",
  136. // 遮罩层
  137. loading: true,
  138. startTime: null,
  139. // 导出遮罩层
  140. exportLoading: false,
  141. // 选中数组
  142. ids: [],
  143. createTime: null,
  144. // 非单个禁用
  145. single: true,
  146. // 非多个禁用
  147. multiple: true,
  148. // 显示搜索条件
  149. showSearch: true,
  150. endTime: null,
  151. // 总条数
  152. total: 0,
  153. // 套餐订单表格数据
  154. packageOrderList: [],
  155. // 弹出层标题
  156. title: "",
  157. // 是否显示弹出层
  158. open: false,
  159. // 是否支付字典
  160. isPayOptions: [],
  161. // 状态字典
  162. statusOptions: [],
  163. refundStatusOptions: [],
  164. packageSubTypeOptions: [],
  165. payTypeOptions: [],
  166. deliveryPayStatusOptions: [],
  167. deliveryStatusOptions: [],
  168. // 查询参数
  169. queryParams: {
  170. pageNum: 1,
  171. pageSize: 10,
  172. orderSn: null,
  173. userId: null,
  174. doctorId: null,
  175. doctorName: null,
  176. phone: null,
  177. phoneMk: null,
  178. packageId: null,
  179. packageName: null,
  180. payMoney: null,
  181. isPay: null,
  182. days: null,
  183. status: null,
  184. startTime: null,
  185. startDate: null,
  186. endDate:null,
  187. finishTime: null,
  188. sTime: null,
  189. eTime: null,
  190. stTime: null,
  191. endTime: null,
  192. endStartTime: null,
  193. endEndTime: null,
  194. companyUserName: null,
  195. companyName: null,
  196. deptId: null,
  197. source: null,
  198. dimension: 'company', // 添加维度参数
  199. courseId: null, // 课程ID
  200. videoId: null // 小节ID
  201. },
  202. // 表单参数
  203. form: {},
  204. // 表单校验
  205. rules: {}
  206. };
  207. },
  208. created() {
  209. // 设置默认时间为前一天
  210. const yesterday = new Date();
  211. yesterday.setDate(yesterday.getDate() - 1);
  212. const formatDate = (date) => {
  213. const year = date.getFullYear();
  214. const month = String(date.getMonth() + 1).padStart(2, '0');
  215. const day = String(date.getDate()).padStart(2, '0');
  216. return `${year}-${month}-${day}`;
  217. };
  218. this.createTime = [formatDate(yesterday), formatDate(yesterday)];
  219. this.queryParams.sTime = this.createTime[0];
  220. this.queryParams.eTime = this.createTime[1];
  221. // 获取课程列表
  222. getCourseList().then(response => {
  223. this.courses = response.data;
  224. })
  225. getCompanyList().then(response => {
  226. this.companys = response.data;
  227. if (this.companys != null && this.companys.length > 0) {
  228. this.companyId = this.companys[0].companyId;
  229. }
  230. this.companys.push({companyId: "-1", companyName: "无"})
  231. });
  232. this.getList();
  233. },
  234. methods: {
  235. /** 查询套餐订单列表 */
  236. getList() {
  237. this.loading = true;
  238. this.queryParams.dimension = this.activeDimension;
  239. let requestParams = { ...this.queryParams };
  240. if (this.activeDimension === 'video') {
  241. // 在小节维度下,如果已选择小节,只保留videoId,不传递courseId
  242. if (requestParams.videoId) {
  243. delete requestParams.courseId;
  244. }
  245. }
  246. courseReport(requestParams).then(response => {
  247. const rows = response.rows || [];
  248. // 标准化数据,为缺失字段提供默认值
  249. this.packageOrderList = rows.map(item => ({
  250. ...item,
  251. // 确保必需字段存在,如果缺失则提供默认值
  252. companyName: item.companyName || '-',
  253. courseName: item.courseName || '-',
  254. videoName: item.videoName || '-',
  255. accessCount: item.accessCount || 0,
  256. finishedCount: item.finishedCount || 0,
  257. courseCompleteTimes: item.courseCompleteTimes || 0,
  258. finishRate: item.finishRate || '0%',
  259. }));
  260. console.log("列表数据:", this.packageOrderList);
  261. this.total = response.total;
  262. this.calculateTotals();
  263. }).catch(error => {
  264. // 即使接口返回错误,也要重置加载状态
  265. console.error('获取数据失败:', error);
  266. this.packageOrderList = [];
  267. this.total = 0;
  268. }).finally(() => {
  269. // 无论成功或失败,都重置加载状态
  270. this.loading = false;
  271. // 延迟强制更新以确保DOM完全渲染
  272. setTimeout(() => {
  273. this.$forceUpdate();
  274. }, 100);
  275. });
  276. },
  277. calculateTotals() {
  278. // 重置总计数据
  279. this.calculatedTotalData = {
  280. accessCount: 0,
  281. finishedCount: 0,
  282. courseCompleteTimes: 0,
  283. finishRate: '0%',
  284. };
  285. // 遍历当前页数据计算总和
  286. this.packageOrderList.forEach(item => {
  287. this.calculatedTotalData.accessCount += Number(item.accessCount) || 0;
  288. this.calculatedTotalData.finishedCount += Number(item.finishedCount) || 0;
  289. this.calculatedTotalData.courseCompleteTimes += Number(item.courseCompleteTimes) || 0;
  290. });
  291. if (this.calculatedTotalData.accessCount > 0) {
  292. // 完课率 = 完课人数 / 进线人数
  293. this.calculatedTotalData.finishRate = ((this.calculatedTotalData.finishedCount / this.calculatedTotalData.accessCount) * 100).toFixed(2) + '%';
  294. } else {
  295. this.calculatedTotalData.finishRate = '0.00%';
  296. }
  297. },
  298. /** 维度切换处理 */
  299. handleDimensionChange(tab) {
  300. this.activeDimension = tab.name;
  301. // 更新查询参数中的维度
  302. this.queryParams.dimension = tab.name;
  303. // 重置相关查询参数
  304. if (this.activeDimension === 'company') {
  305. // 公司维度,清空课程和小节参数
  306. this.queryParams.courseId = null;
  307. this.queryParams.videoId = null;
  308. } else if (this.activeDimension === 'course') {
  309. // 课程维度,清空公司和小节参数
  310. this.queryParams.companyId = null;
  311. this.queryParams.videoId = null;
  312. } else if (this.activeDimension === 'video') {
  313. // 保留 courseId 的选择状态,但清空 videoId
  314. this.queryParams.videoId = null;
  315. }
  316. // 清空小节列表
  317. this.videos = [];
  318. // 重新获取数据
  319. this.getList();
  320. },
  321. /** 课程变更处理 */
  322. handleCourseChange(val) {
  323. // 在课程维度和小节维度都需要处理课程变更
  324. if (this.activeDimension === 'course' || this.activeDimension === 'video') {
  325. this.queryParams.courseId = val;
  326. this.queryParams.videoId = null; // 清空已选择的小节
  327. if (val) {
  328. // 根据课程ID获取对应的小节列表
  329. this.getVideosByCourseId(val);
  330. } else {
  331. // 如果清空课程,也清空小节选项
  332. this.videos = [];
  333. }
  334. }
  335. },
  336. /** 根据训练营获取营期数据 */
  337. getVideosByCourseId(courseId) {
  338. getVideosByCourse(courseId).then((response) => {
  339. this.videos = response.data || [];
  340. }).catch(error => {
  341. console.error('获取小节数据失败:', error);
  342. this.videos = [];
  343. });
  344. },
  345. // 取消按钮
  346. cancel() {
  347. this.open = false;
  348. this.reset();
  349. },
  350. // 表单重置
  351. reset() {
  352. this.form = {
  353. orderId: null,
  354. orderSn: null,
  355. userId: null,
  356. doctorId: null,
  357. packageId: null,
  358. packageName: null,
  359. payMoney: null,
  360. isPay: null,
  361. days: null,
  362. status: 0,
  363. startTime: null,
  364. finishTime: null,
  365. createTime: null
  366. };
  367. this.resetForm("form");
  368. },
  369. /** 搜索按钮操作 */
  370. handleQuery() {
  371. this.getList();
  372. },
  373. /** 重置按钮操作 */
  374. resetQuery() {
  375. this.resetForm("queryForm");
  376. // 清空所有时间相关变量
  377. this.createTime = null;
  378. this.startTime = null;
  379. this.endTime = null;
  380. // 重置所有查询参数
  381. this.queryParams = {
  382. pageNum: null,
  383. pageSize: null,
  384. orderSn: null,
  385. userId: null,
  386. doctorId: null,
  387. doctorName: null,
  388. phone: null,
  389. phoneMk: null,
  390. packageId: null,
  391. packageName: null,
  392. payMoney: null,
  393. isPay: null,
  394. days: null,
  395. status: null,
  396. startTime: null,
  397. finishTime: null,
  398. sTime: null,
  399. eTime: null,
  400. stTime: null,
  401. endTime: null,
  402. endStartTime: null,
  403. endEndTime: null,
  404. companyUserName: null,
  405. companyName: null,
  406. deptId: null,
  407. source: null,
  408. dimension: this.activeDimension, // 维持当前维度
  409. companyId: null, // 重置所有维度ID
  410. courseId: null,
  411. videoId: null,
  412. };
  413. // 重新获取课程列表
  414. getCourseList().then(response => {
  415. this.courses = response.data;
  416. });
  417. this.videos = [];
  418. // 立即执行查询
  419. this.handleQuery();
  420. },
  421. xdChange() {
  422. if (this.createTime != null) {
  423. this.queryParams.sTime = this.createTime[0];
  424. this.queryParams.eTime = this.createTime[1];
  425. } else {
  426. this.queryParams.sTime = null;
  427. this.queryParams.eTime = null;
  428. }
  429. },
  430. handleExport() {
  431. const queryParams = this.queryParams;
  432. this.$confirm('是否确认导出完课统计报表', "警告", {
  433. confirmButtonText: "确定",
  434. cancelButtonText: "取消",
  435. type: "warning"
  436. }).then(() => {
  437. this.exportLoading = true;
  438. return exportCourseReport(queryParams);
  439. }).then(response => {
  440. this.download(response.msg);
  441. this.exportLoading = false;
  442. }).catch(() => {
  443. });
  444. },
  445. endChange() {
  446. if (this.endTime != null) {
  447. this.queryParams.endStartTime = this.endTime[0];
  448. this.queryParams.endEndTime = this.endTime[1];
  449. } else {
  450. this.queryParams.endStartTime = null;
  451. this.queryParams.endEndTime = null;
  452. }
  453. }
  454. }
  455. };
  456. </script>
  457. <style scoped>
  458. .total-summary {
  459. margin-top: 15px;
  460. padding: 15px 20px;
  461. background: linear-gradient(135deg, #f5f7fa 0%, #e4e7f4 100%);
  462. border: 1px solid #dcdfe6;
  463. border-radius: 4px;
  464. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  465. display: flex;
  466. flex-wrap: wrap;
  467. align-items: center;
  468. }
  469. .total-title {
  470. font-weight: bold;
  471. font-size: 16px;
  472. color: #303133;
  473. margin-right: 20px;
  474. flex-shrink: 0;
  475. }
  476. .total-item {
  477. margin-right: 25px;
  478. padding: 5px 10px;
  479. background: white;
  480. border-radius: 3px;
  481. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
  482. display: inline-block;
  483. margin-bottom: 5px;
  484. font-size: 13px;
  485. color: #606266;
  486. }
  487. .total-item::before {
  488. content: "";
  489. display: inline-block;
  490. width: 3px;
  491. height: 3px;
  492. background: #409eff;
  493. border-radius: 50%;
  494. margin-right: 5px;
  495. vertical-align: middle;
  496. }
  497. /* 响应式处理 */
  498. @media (max-width: 768px) {
  499. .total-summary {
  500. flex-direction: column;
  501. align-items: flex-start;
  502. }
  503. .total-title {
  504. margin-bottom: 10px;
  505. }
  506. .total-item {
  507. margin-right: 10px;
  508. margin-bottom: 8px;
  509. }
  510. }
  511. </style>