examSummary.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. <template>
  2. <el-container>
  3. <el-main class="el-main-padding">
  4. <el-row>
  5. <el-col :span="8">
  6. <el-form>
  7. <el-form-item label="考试批次">
  8. <el-select
  9. v-model="examId"
  10. filterable
  11. remote
  12. :remote-method="getExams"
  13. clearable
  14. @clear="getExams"
  15. @change="changeExam"
  16. placeholder="请选择考试批次"
  17. size="small"
  18. >
  19. <el-option
  20. v-for="item in examList"
  21. :key="item.id"
  22. :label="item.name"
  23. :value="item.id"
  24. ></el-option>
  25. </el-select>
  26. </el-form-item>
  27. </el-form>
  28. </el-col>
  29. <el-col :span="16">
  30. <el-form>
  31. <el-form-item label="考试数据同步状态">
  32. <el-progress
  33. :text-inside="true"
  34. :stroke-width="22"
  35. :percentage="examSyncPercentage"
  36. status="success"
  37. style="width: 200px; float: left; line-height: 38px;"
  38. ></el-progress>
  39. <el-button
  40. size="small"
  41. type="primary"
  42. icon="el-icon-refresh"
  43. style="float: left; margin-left: 20px; margin-top: 4px;"
  44. @click="refreshExamSyncPercentage"
  45. >刷新</el-button
  46. >
  47. </el-form-item>
  48. </el-form>
  49. </el-col>
  50. </el-row>
  51. <el-row :gutter="2">
  52. <el-col :span="10" class="chart-border">
  53. <div class="chart-header">考试进度情况</div>
  54. <div>
  55. <v-chart :options="pieOptions" />
  56. </div>
  57. </el-col>
  58. <el-col :span="14" class="chart-border">
  59. <div class="chart-header">课程完成进度TOP5</div>
  60. <div>
  61. <v-chart :options="lineOptions" />
  62. </div>
  63. </el-col>
  64. </el-row>
  65. <el-row style="margin-top: 10px;">
  66. <el-col :span="24">
  67. <el-tabs type="card" v-model="activeName" @tab-click="handleClick">
  68. <el-tab-pane label="学习中心完成进度" name="first">
  69. <el-row style="margin-top: 20px;">
  70. <el-col :span="24">
  71. <el-form>
  72. <el-form-item label="学习中心">
  73. <el-select
  74. v-model="orgId"
  75. filterable
  76. remote
  77. :remote-method="getOrgs"
  78. clearable
  79. @clear="getOrgs"
  80. @change="getOrgExamInfos"
  81. placeholder="请选择学习中心"
  82. size="small"
  83. >
  84. <el-option
  85. v-for="item in orgList"
  86. :key="item.id"
  87. :label="item.name"
  88. :value="item.id"
  89. ></el-option>
  90. </el-select>
  91. </el-form-item>
  92. <el-form-item>
  93. <el-button
  94. type="primary"
  95. size="small"
  96. icon="el-icon-download"
  97. @click="exportOrg"
  98. v-show="!exportOrgLoading"
  99. >导出</el-button
  100. >
  101. <el-button
  102. size="small"
  103. icon="el-icon-download"
  104. :loading="true"
  105. v-show="exportOrgLoading"
  106. >导出数据中...</el-button
  107. >
  108. </el-form-item>
  109. </el-form>
  110. </el-col>
  111. <el-col :span="24">
  112. <el-table
  113. element-loading-text="数据加载中"
  114. :data="orgExamInfos"
  115. border
  116. >
  117. <el-table-column
  118. sortable
  119. label="学习中心代码"
  120. prop="orgCode"
  121. ></el-table-column>
  122. <el-table-column
  123. sortable
  124. label="学习中心名称"
  125. prop="orgName"
  126. ></el-table-column>
  127. <el-table-column
  128. sortable
  129. label="报考人数"
  130. prop="totalCount"
  131. ></el-table-column>
  132. <el-table-column
  133. sortable
  134. label="实考人数"
  135. prop="finishedCount"
  136. ></el-table-column>
  137. <el-table-column
  138. sortable
  139. :sort-method="sortByFinishedPercent"
  140. label="完成比率"
  141. prop="finishedPercent"
  142. ></el-table-column>
  143. </el-table>
  144. </el-col>
  145. </el-row>
  146. </el-tab-pane>
  147. <el-tab-pane label="课程完成进度" name="two">
  148. <el-row style="margin-top: 20px;">
  149. <el-col :span="24">
  150. <el-form>
  151. <el-form-item label="课程">
  152. <el-select
  153. v-model="courseId"
  154. filterable
  155. remote
  156. :remote-method="getCourses"
  157. clearable
  158. @clear="getCourses"
  159. @change="getCourseProgress"
  160. placeholder="请选择课程"
  161. size="small"
  162. >
  163. <el-option
  164. v-for="item in courseList"
  165. :key="item.id"
  166. :label="item.name"
  167. :value="item.id"
  168. ></el-option>
  169. </el-select>
  170. </el-form-item>
  171. <el-form-item>
  172. <el-button
  173. type="primary"
  174. size="small"
  175. icon="el-icon-download"
  176. @click="exportCourse"
  177. v-show="!exportCourseLoading"
  178. >导出</el-button
  179. >
  180. <el-button
  181. size="small"
  182. icon="el-icon-download"
  183. :loading="true"
  184. v-show="exportCourseLoading"
  185. >导出数据中...</el-button
  186. >
  187. </el-form-item>
  188. </el-form>
  189. </el-col>
  190. <el-col :span="24">
  191. <el-table
  192. element-loading-text="数据加载中"
  193. :data="courseProgressList"
  194. border
  195. >
  196. <el-table-column
  197. sortable
  198. label="课程名称"
  199. prop="courseName"
  200. ></el-table-column>
  201. <el-table-column
  202. sortable
  203. label="课程代码"
  204. prop="courseCode"
  205. ></el-table-column>
  206. <el-table-column
  207. sortable
  208. label="报考人数"
  209. prop="allNum"
  210. ></el-table-column>
  211. <el-table-column
  212. sortable
  213. label="实考人数"
  214. prop="completedNum"
  215. ></el-table-column>
  216. <el-table-column
  217. sortable
  218. label="完成比率"
  219. prop="completedProportion"
  220. ></el-table-column>
  221. </el-table>
  222. </el-col>
  223. </el-row>
  224. </el-tab-pane>
  225. </el-tabs>
  226. </el-col>
  227. </el-row>
  228. </el-main>
  229. </el-container>
  230. </template>
  231. <script>
  232. import { mapState } from "vuex";
  233. import ECharts from "vue-echarts/components/ECharts";
  234. import "echarts/lib/component/legend";
  235. import "echarts/lib/component/legendScroll";
  236. import "echarts/lib/chart/pie";
  237. import "echarts/lib/component/polar";
  238. import "echarts/lib/component/tooltip";
  239. import "echarts/lib/component/title";
  240. import "echarts/lib/chart/bar";
  241. import "echarts/lib/chart/line";
  242. export default {
  243. components: { "v-chart": ECharts },
  244. data() {
  245. return {
  246. examList: [],
  247. orgList: [],
  248. courseList: [],
  249. examId: "",
  250. orgId: "",
  251. courseId: "",
  252. activeName: "first",
  253. orgExamInfos: [],
  254. courseProgressList: [],
  255. lineOptions: {},
  256. pieOptions: {},
  257. exportOrgLoading: false,
  258. exportCourseLoading: false,
  259. examSyncPercentage: 0
  260. };
  261. },
  262. computed: {
  263. ...mapState({ user: state => state.user })
  264. },
  265. methods: {
  266. getExams(examName) {
  267. if (!examName) {
  268. examName = "";
  269. }
  270. this.$http
  271. .get("/api/ecs_exam_work/exam/queryByNameLike", {
  272. params: {
  273. name: examName,
  274. examTypes: "ONLINE#OFFLINE#ONLINE_HOMEWORK"
  275. }
  276. })
  277. .then(response => {
  278. this.examList = response.data;
  279. });
  280. },
  281. getOrgs(orgName) {
  282. if (!orgName) {
  283. orgName = "";
  284. }
  285. var rootOrgId = this.user.rootOrgId;
  286. this.$http
  287. .get("/api/ecs_core/org/query", {
  288. params: {
  289. name: orgName,
  290. rootOrgId: rootOrgId,
  291. enable: true
  292. }
  293. })
  294. .then(response => {
  295. this.orgList = response.data;
  296. });
  297. },
  298. handleClick(tab, event) {
  299. console.log(tab, event);
  300. },
  301. getOrgExamInfos() {
  302. this.$http
  303. .post(
  304. "/api/ecs_oe_admin/exam/student/statistic/by/org?examId=" +
  305. this.examId +
  306. "&orgId=" +
  307. this.orgId
  308. )
  309. .then(response => {
  310. if (response.data && response.data.length > 0) {
  311. this.orgExamInfos = response.data;
  312. } else {
  313. this.orgExamInfos = [];
  314. }
  315. });
  316. },
  317. getCourseProgress() {
  318. this.$http
  319. .get("/api/ecs_oe_admin/exam/student/courseProgress/list", {
  320. params: {
  321. examId: this.examId,
  322. courseId: this.courseId
  323. }
  324. })
  325. .then(response => {
  326. if (response.data && response.data.length > 0) {
  327. this.courseProgressList = response.data;
  328. this.buildLine(response.data);
  329. } else {
  330. this.courseProgressList = [];
  331. this.lineOptions = {};
  332. }
  333. });
  334. },
  335. getCourses() {
  336. if (!this.examId) {
  337. return false;
  338. }
  339. this.$http
  340. .get("/api/ecs_oe_admin/exam/student/findCoursesByExamIdAndOrgId", {
  341. params: {
  342. examId: this.examId,
  343. orgId: this.orgId
  344. }
  345. })
  346. .then(response => {
  347. if (response.data && response.data.length > 0) {
  348. this.courseList = response.data;
  349. } else {
  350. this.courseList = [];
  351. }
  352. });
  353. },
  354. changeExam(examId) {
  355. var exam = this.examList.filter(item => {
  356. return item.id == examId;
  357. })[0];
  358. this.getPieData(exam.examType);
  359. this.getCourses();
  360. this.getOrgExamInfos();
  361. this.getCourseProgress();
  362. },
  363. getPieData(examType) {
  364. var completedWord =
  365. examType == "ONLINE" || examType == "ONLINE_HOMEWORK"
  366. ? "已完成:"
  367. : "已抽题:";
  368. var noCompletedWord =
  369. examType == "ONLINE" || examType == "ONLINE_HOMEWORK"
  370. ? "未完成:"
  371. : "未抽题:";
  372. if (!this.examId) {
  373. return;
  374. }
  375. this.$http
  376. .post(
  377. "/api/ecs_oe_admin/exam/student/statistic/by/finished?examId=" +
  378. this.examId
  379. )
  380. .then(response => {
  381. var resp = response.data;
  382. var optionData = {
  383. title: "考试人次:" + (resp.finished + resp.unFinished),
  384. legendData: [
  385. noCompletedWord + resp.unFinished,
  386. completedWord + resp.finished
  387. ],
  388. seriesData: [
  389. {
  390. name: noCompletedWord + resp.unFinished,
  391. value: resp.unFinished
  392. },
  393. {
  394. name: completedWord + resp.finished,
  395. value: resp.finished
  396. }
  397. ]
  398. };
  399. this.buildPieOptions(optionData);
  400. });
  401. },
  402. buildPieOptions(data) {
  403. var colors = ["#7CB5EC", "#FE8463"];
  404. this.pieOptions = {
  405. color: colors,
  406. title: {
  407. text: data.title,
  408. subtext: "",
  409. x: "left"
  410. },
  411. tooltip: {
  412. trigger: "item",
  413. formatter: "{b}人次<br/>占比:{d}%"
  414. },
  415. legend: {
  416. type: "scroll",
  417. orient: "vertical",
  418. right: 200,
  419. top: 30,
  420. data: data.legendData
  421. },
  422. series: [
  423. {
  424. name: "",
  425. type: "pie",
  426. radius: "50%",
  427. center: ["35%", "60%"],
  428. data: data.seriesData,
  429. itemStyle: {
  430. emphasis: {
  431. shadowBlur: 10,
  432. shadowOffsetX: 0,
  433. shadowColor: "rgba(0, 0, 0, 0.5)"
  434. }
  435. }
  436. }
  437. ]
  438. };
  439. },
  440. buildLine(courseProgressList) {
  441. courseProgressList.sort(function(a, b) {
  442. if (b["completedProportion"] != a["completedProportion"]) {
  443. return b["completedProportion"] - a["completedProportion"];
  444. } else if (b["allNum"] != a["allNum"]) {
  445. return b["allNum"] - a["allNum"];
  446. } else {
  447. return b["completedNum"] - a["completedNum"];
  448. }
  449. });
  450. var campusCount = 5;
  451. var courseProgressDataList = [];
  452. //找出5个完成比例最高的
  453. if (courseProgressList.length >= campusCount) {
  454. courseProgressDataList = courseProgressList.slice(0, campusCount);
  455. } else {
  456. courseProgressDataList = courseProgressList;
  457. }
  458. var xAxisData = [];
  459. var seriesBar = {
  460. name: "计划数",
  461. type: "bar",
  462. data: []
  463. };
  464. var seriesLine1 = {
  465. name: "完成数",
  466. type: "line",
  467. data: []
  468. };
  469. var seriesLine2 = {
  470. name: "完成比(%)",
  471. type: "line",
  472. yAxisIndex: 1,
  473. data: []
  474. };
  475. var yAxis_maxScale1 = 0;
  476. for (var i = 0; i < courseProgressDataList.length; i++) {
  477. xAxisData.push(courseProgressDataList[i].courseName);
  478. seriesBar.data.push(courseProgressDataList[i].allNum);
  479. seriesLine1.data.push(courseProgressDataList[i].completedNum);
  480. seriesLine2.data.push(courseProgressDataList[i].completedProportion);
  481. if (courseProgressDataList[i].allNum > yAxis_maxScale1) {
  482. yAxis_maxScale1 = courseProgressDataList[i].allNum;
  483. }
  484. }
  485. var optionData = {
  486. legendData: ["计划数", "完成数", "完成比(%)"],
  487. xAxis: { data: xAxisData },
  488. series: [seriesBar, seriesLine1, seriesLine2],
  489. yAxis_maxScale1
  490. };
  491. this.buildLineOptions(optionData);
  492. },
  493. buildLineOptions(optionData) {
  494. var colors = ["#FE8463", "#66CCFF", "#675bba"];
  495. this.lineOptions = {
  496. color: colors,
  497. tooltip: {
  498. trigger: "axis",
  499. axisPointer: {
  500. type: "cross",
  501. crossStyle: {
  502. color: "#999"
  503. }
  504. }
  505. },
  506. toolbox: {
  507. feature: {
  508. dataView: { show: true, readOnly: false },
  509. magicType: { show: true, type: ["line", "bar"] },
  510. restore: { show: true },
  511. saveAsImage: { show: true }
  512. }
  513. },
  514. legend: {
  515. data: optionData.legendData
  516. },
  517. xAxis: [
  518. {
  519. type: "category",
  520. data: optionData.xAxis.data,
  521. axisPointer: {
  522. type: "shadow"
  523. }
  524. }
  525. ],
  526. yAxis: [
  527. {
  528. type: "value",
  529. name: "人次",
  530. min: 0,
  531. max: optionData.yAxis_maxScale1,
  532. interval: 10000,
  533. axisLabel: {
  534. formatter: "{value} "
  535. }
  536. },
  537. {
  538. type: "value",
  539. name: "完成比例",
  540. min: 0,
  541. max: 100,
  542. interval: 20,
  543. axisLabel: {
  544. formatter: "{value} %"
  545. }
  546. }
  547. ],
  548. series: optionData.series
  549. };
  550. },
  551. sortByFinishedPercent(obj1, obj2) {
  552. let p1 = Number(obj1.finishedPercent);
  553. let p2 = Number(obj2.finishedPercent);
  554. return p1 - p2;
  555. },
  556. exportOrg() {
  557. this.exportOrgLoading = true;
  558. this.$http
  559. .get("/api/ecs_oe_admin/exam/student/statistic/by/org/export", {
  560. params: {
  561. examId: this.examId,
  562. orgId: this.orgId
  563. },
  564. responseType: "arraybuffer",
  565. timeout: 20 * 60 * 1000 //限时20分钟
  566. })
  567. .then(response => {
  568. if (response.data) {
  569. var blob = new Blob([response.data], {
  570. type:
  571. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  572. });
  573. var url = URL.createObjectURL(blob);
  574. var a = document.createElement("a");
  575. a.href = url;
  576. a.download = "学习中心完成进度.xlsx";
  577. a.target = "_blank";
  578. a.click();
  579. URL.revokeObjectURL(url);
  580. }
  581. this.exportOrgLoading = false;
  582. });
  583. },
  584. exportCourse() {
  585. this.exportCourseLoading = true;
  586. this.$http
  587. .get("/api/ecs_oe_admin/exam/student/courseProgress/list/export", {
  588. params: {
  589. examId: this.examId,
  590. courseId: this.courseId
  591. },
  592. responseType: "arraybuffer",
  593. timeout: 20 * 60 * 1000 //限时20分钟
  594. })
  595. .then(response => {
  596. if (response.data) {
  597. var blob = new Blob([response.data], {
  598. type:
  599. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  600. });
  601. var url = URL.createObjectURL(blob);
  602. var a = document.createElement("a");
  603. a.href = url;
  604. a.download = "课程完成进度.xlsx";
  605. a.target = "_blank";
  606. a.click();
  607. URL.revokeObjectURL(url);
  608. }
  609. this.exportCourseLoading = false;
  610. });
  611. },
  612. refreshExamSyncPercentage() {
  613. if (!this.examId) {
  614. this.$notify({
  615. title: "警告",
  616. message: "请选择考试批次",
  617. type: "warning",
  618. duration: 2000
  619. });
  620. return false;
  621. }
  622. this.$http
  623. .get("/api/ecs_oe_admin/examControl/getExamSyncPercentage", {
  624. params: { examId: this.examId }
  625. })
  626. .then(response => {
  627. this.examSyncPercentage = response.data;
  628. })
  629. .catch(res => {
  630. var errorMsg = "操作失败";
  631. if (res.response && res.response.data) {
  632. errorMsg = res.response.data.desc;
  633. }
  634. this.$notify({
  635. title: "提示",
  636. message: errorMsg,
  637. type: "error"
  638. });
  639. });
  640. }
  641. },
  642. created() {
  643. this.getExams();
  644. this.getOrgs();
  645. this.refreshExamSyncPercentage();
  646. }
  647. };
  648. </script>
  649. <style>
  650. .chart-border {
  651. border: 1px solid #ddd;
  652. }
  653. .chart-header {
  654. color: #333;
  655. font-size: 14px;
  656. background-color: #f5f5f5;
  657. border-color: #ddd;
  658. padding: 10px 15px;
  659. border-bottom: 1px solid transparent;
  660. border-top-left-radius: 3px;
  661. border-top-right-radius: 3px;
  662. }
  663. </style>
  664. <style scoped src="../style/common.css"></style>