courseReport.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. <template>
  2. <div class="app-container">
  3. <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="85px">
  4. <el-radio-group v-model="queryParams.dimension" @change="handleDimensionChange">
  5. <el-radio-button label="company">销售公司</el-radio-button>
  6. <el-radio-button label="camp">训练营</el-radio-button>
  7. </el-radio-group>
  8. <el-form-item label="公司名" prop="companyId">
  9. <el-select filterable v-model="queryParams.companyId" placeholder="请选择公司名"
  10. clearable size="small">
  11. <el-option
  12. v-for="item in companys"
  13. :key="item.companyId"
  14. :label="item.companyName"
  15. :value="item.companyId"
  16. />
  17. </el-select>
  18. </el-form-item>
  19. <el-form-item label="训练营" prop="trainingCampId" v-if="queryParams.dimension === 'camp'">
  20. <el-select filterable v-model="queryParams.trainingCampId" placeholder="请选择训练营"
  21. clearable size="small" @change="handleCampChange">
  22. <el-option
  23. v-for="item in camps"
  24. :key="item.dictValue"
  25. :label="item.dictLabel"
  26. :value="item.dictValue"
  27. />
  28. </el-select>
  29. </el-form-item>
  30. <el-form-item>
  31. <treeselect style="width: 220px" v-model="queryParams.periodId" :options="deptOptions"
  32. clearable :show-count="true" placeholder="请选择归属营期" value-consists-of="LEAF_PRIORITY"
  33. :normalizer="normalizer" v-if="queryParams.dimension === 'camp'" />
  34. </el-form-item>
  35. <el-form-item>
  36. <el-form-item label="看课时间" prop="createTime">
  37. <el-date-picker v-model="createTime" size="small" style="width: 220px" value-format="yyyy-MM-dd"
  38. type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
  39. @change="xdChange"></el-date-picker>
  40. </el-form-item>
  41. <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  42. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  43. </el-form-item>
  44. </el-form>
  45. <el-row :gutter="10" class="mb8">
  46. <el-col :span="1.5">
  47. <el-button
  48. type="warning"
  49. plain
  50. icon="el-icon-download"
  51. size="mini"
  52. :loading="exportLoading"
  53. @click="handleExport"
  54. >导出</el-button>
  55. </el-col>
  56. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  57. </el-row>
  58. <el-table height="500" v-loading="loading" border :data="packageOrderList">
  59. <el-table-column label="销售公司" align="center" prop="companyName" width="120px"/>
  60. <el-table-column label="训练营" align="center" prop="trainingCampName" v-if="showTrainingCampColumn"/>
  61. <el-table-column label="营期" align="center" prop="periodName" v-if="showPeriodColumn"/>
  62. <el-table-column label="进线人数" align="center" prop="accessCount"/>
  63. <el-table-column label="待看课人数" align="center" prop="pendingCount"/>
  64. <el-table-column label="看课中人数" align="center" prop="watchingCount"/>
  65. <el-table-column label="看课率" align="center" prop="watchRate"/>
  66. <el-table-column label="完课人数" align="center" prop="finishedCount"/>
  67. <el-table-column label="看课中断" align="center" prop="interruptedCount"/>
  68. <el-table-column label="完课率" align="center" prop="finishRate"/>
  69. <el-table-column label="答题人数" align="center" prop="answerUserCount"/>
  70. <el-table-column label="红包领取人数" align="center" prop="packetUserCount"/>
  71. <el-table-column label="红包金额" align="center" prop="packetAmount"/>
  72. </el-table>
  73. <div class="total-summary">
  74. <span class="total-title">总计:</span>
  75. <span class="total-item">进线人数: {{ calculatedTotalData.accessCount }}</span>
  76. <span class="total-item">待看课人数: {{ calculatedTotalData.pendingCount }}</span>
  77. <span class="total-item">看课中人数: {{ calculatedTotalData.watchingCount }}</span>
  78. <span class="total-item">看课率: {{ calculatedTotalData.watchRate}}</span>
  79. <span class="total-item">完课人数: {{ calculatedTotalData.finishedCount }}</span>
  80. <span class="total-item">看课中断: {{ calculatedTotalData.interruptedCount }}</span>
  81. <span class="total-item">完课率: {{ calculatedTotalData.finishRate}}</span>
  82. <span class="total-item">答题人数: {{ calculatedTotalData.answerUserCount }}</span>
  83. <span class="total-item">红包领取人数: {{ calculatedTotalData.packetUserCount}}</span>
  84. <span class="total-item">红包金额: {{ calculatedTotalData.packetAmount }}</span>
  85. </div>
  86. <pagination
  87. v-show="total>0"
  88. :total="total"
  89. :page.sync="queryParams.pageNum"
  90. :limit.sync="queryParams.pageSize"
  91. @pagination="getList"
  92. />
  93. </div>
  94. </template>
  95. <script>
  96. import {
  97. listPackageOrder, getPackageOrder, delPackageOrder, addPackageOrder, updatePackageOrder, exportPackageOrder,
  98. PackageOrderReport, orderReport, courseReport, exportOrderReport, exportCourseReport
  99. } from "@/api/his/packageOrder";
  100. import {getCompanyList} from "@/api/company/company";
  101. import packageOrderDetails from '../../components/his/packageOrderDetails.vue';
  102. import {treeselect} from "@/api/company/companyDept";
  103. import Treeselect from "@riophae/vue-treeselect";
  104. import "@riophae/vue-treeselect/dist/vue-treeselect.css";
  105. import {getTask} from "@/api/common";
  106. import {getCampList, getPeriodList} from "@/api/course/userWatchCourseStatistics";
  107. export default {
  108. name: "PackageOrder",
  109. components: {packageOrderDetails, Treeselect},
  110. data() {
  111. return {
  112. normalizer: function(node) {
  113. return {
  114. id: node.id || node.dictValue,
  115. label: node.label || node.dictLabel,
  116. children: node.children
  117. }
  118. },
  119. // 添加用于存储计算总和的数据
  120. calculatedTotalData: {
  121. accessCount: 0,
  122. pendingCount: 0,
  123. watchingCount: 0,
  124. watchRate: '0%',
  125. finishedCount: 0,
  126. interruptedCount: 0,
  127. finishRate: '0%',
  128. answerUserCount: 0,
  129. packetUserCount: 0,
  130. packetAmount: 0
  131. },
  132. showTrainingCampColumn: false,
  133. showPeriodColumn: false,
  134. totalData: {},
  135. companys: [],
  136. camps: [],
  137. deptOptions: [],
  138. companyId: undefined,
  139. campId: undefined,
  140. deptId: undefined,
  141. show: {
  142. open: false,
  143. },
  144. sourceOptions: [],
  145. actName: "2",
  146. // 遮罩层
  147. loading: true,
  148. startTime: null,
  149. // 导出遮罩层
  150. exportLoading: false,
  151. // 选中数组
  152. ids: [],
  153. createTime: null,
  154. // 非单个禁用
  155. single: true,
  156. // 非多个禁用
  157. multiple: true,
  158. // 显示搜索条件
  159. showSearch: true,
  160. endTime: null,
  161. // 总条数
  162. total: 0,
  163. // 套餐订单表格数据
  164. packageOrderList: [],
  165. // 弹出层标题
  166. title: "",
  167. // 是否显示弹出层
  168. open: false,
  169. // 是否支付字典
  170. isPayOptions: [],
  171. // 状态字典
  172. statusOptions: [],
  173. refundStatusOptions: [],
  174. packageSubTypeOptions: [],
  175. payTypeOptions: [],
  176. deliveryPayStatusOptions: [],
  177. deliveryStatusOptions: [],
  178. // 查询参数
  179. queryParams: {
  180. pageNum: 1,
  181. pageSize: 10,
  182. orderSn: null,
  183. userId: null,
  184. doctorId: null,
  185. doctorName: null,
  186. phone: null,
  187. phoneMk: null,
  188. packageId: null,
  189. packageName: null,
  190. payMoney: null,
  191. isPay: null,
  192. days: null,
  193. status: null,
  194. startTime: null,
  195. startDate: null,
  196. endDate:null,
  197. finishTime: null,
  198. sTime: null,
  199. eTime: null,
  200. stTime: null,
  201. endTime: null,
  202. endStartTime: null,
  203. endEndTime: null,
  204. companyUserName: null,
  205. companyName: null,
  206. deptId: null,
  207. source: null,
  208. trainingCampId: null,
  209. periodId: null,
  210. dimension: 'company'
  211. },
  212. // 表单参数
  213. form: {},
  214. // 表单校验
  215. rules: {}
  216. };
  217. },
  218. created() {
  219. getCampList().then(response => {
  220. this.camps = response.data.list
  221. if (this.camps != null && this.camps.length > 0) {
  222. this.companyId = this.camps[0].dictValue;
  223. }
  224. this.camps.push({companyId: "-1", companyName: "无"})
  225. });
  226. getCompanyList().then(response => {
  227. this.companys = response.data;
  228. if (this.companys != null && this.companys.length > 0) {
  229. this.companyId = this.companys[0].companyId;
  230. }
  231. this.companys.push({companyId: "-1", companyName: "无"})
  232. });
  233. this.getList();
  234. this.getDicts("sys_company_or").then(response => {
  235. this.isPayOptions = response.data;
  236. });
  237. this.getDicts("sys_package_order_status").then(response => {
  238. this.statusOptions = response.data;
  239. });
  240. this.getDicts("sys_refund_status").then(response => {
  241. this.refundStatusOptions = response.data;
  242. });
  243. this.getDicts("sys_order_source").then(response => {
  244. this.sourceOptions = response.data;
  245. });
  246. this.getDicts("sys_package_pay_type").then(response => {
  247. this.payTypeOptions = response.data;
  248. });
  249. this.getDicts("sys_store_delivery_pay_status").then(response => {
  250. this.deliveryPayStatusOptions = response.data;
  251. });
  252. this.getDicts("sys_store_order_delivery_status").then(response => {
  253. this.deliveryStatusOptions = response.data;
  254. });
  255. this.getDicts("sys_package_sub_type").then(response => {
  256. this.packageSubTypeOptions = response.data;
  257. });
  258. },
  259. methods: {
  260. handleDimensionChange: function(val) {
  261. console.log('维度切换到:', val);
  262. // 重置分页
  263. this.queryParams.pageNum = 1;
  264. // 重新获取数据
  265. this.getList();
  266. },
  267. /** 查询套餐订单列表 */
  268. getList() {
  269. this.loading = true;
  270. courseReport(this.queryParams).then(response => {
  271. this.packageOrderList = response.rows;
  272. this.total = response.total;
  273. // 判断是否需要显示训练营和营期列
  274. this.showTrainingCampColumn = this.packageOrderList &&
  275. this.packageOrderList.some(item => item.trainingCampName);
  276. this.showPeriodColumn = this.packageOrderList &&
  277. this.packageOrderList.some(item => item.periodName);
  278. this.calculateTotals();
  279. this.loading = false;
  280. // 延迟强制更新以确保DOM完全渲染
  281. setTimeout(() => {
  282. this.$forceUpdate();
  283. }, 100);
  284. });
  285. },
  286. calculateTotals() {
  287. // 重置总计数据
  288. this.calculatedTotalData = {
  289. accessCount: 0,
  290. pendingCount: 0,
  291. watchingCount: 0,
  292. watchRate: 0,
  293. finishedCount: 0,
  294. interruptedCount: 0,
  295. finishRate: 0,
  296. answerUserCount: 0,
  297. packetUserCount: 0,
  298. packetAmount: 0
  299. };
  300. // 遍历当前页数据计算总和
  301. this.packageOrderList.forEach(item => {
  302. this.calculatedTotalData.accessCount += Number(item.accessCount) || 0;
  303. this.calculatedTotalData.pendingCount += Number(item.pendingCount) || 0;
  304. this.calculatedTotalData.watchingCount += Number(item.watchingCount) || 0;
  305. this.calculatedTotalData.finishedCount += Number(item.finishedCount) || 0;
  306. this.calculatedTotalData.interruptedCount += Number(item.interruptedCount) || 0;
  307. this.calculatedTotalData.answerUserCount += Number(item.answerUserCount) || 0;
  308. this.calculatedTotalData.packetUserCount += Number(item.packetUserCount) || 0;
  309. this.calculatedTotalData.packetAmount += Number(item.packetAmount) || 0;
  310. });
  311. if (this.calculatedTotalData.accessCount > 0) {
  312. // 看课率 = 看课中人数 / 进线人数
  313. this.calculatedTotalData.watchRate = ((this.calculatedTotalData.watchingCount / this.calculatedTotalData.accessCount) * 100).toFixed(2) + '%';
  314. // 完课率 = 完课人数 / 进线人数
  315. this.calculatedTotalData.finishRate = ((this.calculatedTotalData.finishedCount / this.calculatedTotalData.accessCount) * 100).toFixed(2) + '%';
  316. } else {
  317. this.calculatedTotalData.watchRate = '0.00%';
  318. this.calculatedTotalData.finishRate = '0.00%';
  319. }
  320. // 金额格式化
  321. this.calculatedTotalData.packetAmount = this.calculatedTotalData.packetAmount.toFixed(2);
  322. },
  323. /** 训练营变更处理 */
  324. handleCampChange(val) {
  325. this.queryParams.trainingCampId = val;
  326. this.queryParams.periodId = null; // 清空已选择的营期
  327. if (val) {
  328. // 获取对应的营期数据
  329. this.getPeriodByCamp(val);
  330. } else {
  331. // 如果清空训练营,也清空营期选项
  332. this.deptOptions = [];
  333. }
  334. // 移除了自动搜索的逻辑,只在点击搜索按钮时触发
  335. },
  336. /** 根据训练营获取营期数据 */
  337. getPeriodByCamp(campId) {
  338. const param = {campId: campId};
  339. getPeriodList(param).then((response) => {
  340. console.log('接口返回数据:', response);
  341. console.log('营期列表数据:', response.data);
  342. this.deptOptions = response.data || [];
  343. console.log('deptOptions 已赋值:', this.deptOptions);
  344. }).catch(error => {
  345. console.error('获取营期数据失败:', error);
  346. this.deptOptions = [];
  347. });
  348. },
  349. hasDeptNameField() {
  350. return this.packageOrderList &&
  351. this.packageOrderList.some(item => item.deptName !== undefined && item.deptName !== null && item.deptName !== '');
  352. },
  353. // 取消按钮
  354. cancel() {
  355. this.open = false;
  356. this.reset();
  357. },
  358. // 表单重置
  359. reset() {
  360. this.form = {
  361. orderId: null,
  362. orderSn: null,
  363. userId: null,
  364. doctorId: null,
  365. packageId: null,
  366. packageName: null,
  367. payMoney: null,
  368. isPay: null,
  369. days: null,
  370. status: 0,
  371. startTime: null,
  372. finishTime: null,
  373. createTime: null
  374. };
  375. this.resetForm("form");
  376. },
  377. /** 搜索按钮操作 */
  378. handleQuery() {
  379. this.getList();
  380. },
  381. /** 重置按钮操作 */
  382. resetQuery() {
  383. this.resetForm("queryForm");
  384. // 清空所有时间相关变量
  385. this.createTime = null;
  386. this.startTime = null;
  387. this.endTime = null;
  388. // 重置所有查询参数
  389. this.queryParams = {
  390. pageNum: null,
  391. pageSize: null,
  392. orderSn: null,
  393. userId: null,
  394. doctorId: null,
  395. doctorName: null,
  396. phone: null,
  397. phoneMk: null,
  398. packageId: null,
  399. packageName: null,
  400. payMoney: null,
  401. isPay: null,
  402. days: null,
  403. status: null,
  404. startTime: null,
  405. finishTime: null,
  406. sTime: null,
  407. eTime: null,
  408. stTime: null,
  409. endTime: null,
  410. endStartTime: null,
  411. endEndTime: null,
  412. companyUserName: null,
  413. companyName: null,
  414. companyId: null,
  415. deptId: null,
  416. source: null,
  417. };
  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>