examSummary.vue 23 KB

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