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. @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="7">
  30. <el-form>
  31. <el-form-item label="场次">
  32. <el-select
  33. clearable
  34. :disabled="examStageDisabled4Search"
  35. :remote-method="queryExamStages4Search"
  36. remote
  37. :loading="queryExamStages4SearchLoading"
  38. :filterable="false"
  39. v-model="examStageId"
  40. placeholder="请选择"
  41. size="small"
  42. @change="changeExamStage"
  43. >
  44. <el-option
  45. v-for="item in examStageList4Search"
  46. :label="item.stageOrder"
  47. :value="item.id"
  48. :key="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 type="card" v-model="activeName" @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. @clear="getOrgs"
  105. @change="getOrgExamInfos"
  106. placeholder="请选择学习中心"
  107. size="small"
  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. type="primary"
  120. size="small"
  121. icon="el-icon-download"
  122. @click="exportOrg"
  123. v-show="!exportOrgLoading"
  124. >导出</el-button
  125. >
  126. <el-button
  127. size="small"
  128. icon="el-icon-download"
  129. :loading="true"
  130. v-show="exportOrgLoading"
  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. @clear="getCourses"
  184. @change="getCourseProgress"
  185. placeholder="请选择课程"
  186. size="small"
  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. type="primary"
  199. size="small"
  200. icon="el-icon-download"
  201. @click="exportCourse"
  202. v-show="!exportCourseLoading"
  203. >导出</el-button
  204. >
  205. <el-button
  206. size="small"
  207. icon="el-icon-download"
  208. :loading="true"
  209. v-show="exportCourseLoading"
  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. methods: {
  297. getExams(examName) {
  298. if (!examName) {
  299. examName = "";
  300. }
  301. this.$http
  302. .get("/api/ecs_exam_work/exam/queryByNameLike", {
  303. params: {
  304. name: examName,
  305. examTypes: "ONLINE#OFFLINE#ONLINE_HOMEWORK"
  306. }
  307. })
  308. .then(response => {
  309. this.examList = response.data;
  310. });
  311. },
  312. getOrgs(orgName) {
  313. if (!orgName) {
  314. orgName = "";
  315. }
  316. var rootOrgId = this.user.rootOrgId;
  317. this.$http
  318. .get("/api/ecs_core/org/query", {
  319. params: {
  320. name: orgName,
  321. rootOrgId: rootOrgId,
  322. enable: true
  323. }
  324. })
  325. .then(response => {
  326. this.orgList = response.data;
  327. });
  328. },
  329. handleClick(tab, event) {
  330. console.log(tab, event);
  331. },
  332. getOrgExamInfos() {
  333. this.$http
  334. .post(
  335. "/api/ecs_oe_admin/exam/student/statistic/by/org?examId=" +
  336. this.examId +
  337. "&examStageId=" +
  338. this.examStageId +
  339. "&orgId=" +
  340. this.orgId
  341. )
  342. .then(response => {
  343. if (response.data && response.data.length > 0) {
  344. this.orgExamInfos = response.data;
  345. } else {
  346. this.orgExamInfos = [];
  347. }
  348. });
  349. },
  350. getCourseProgress() {
  351. this.$http
  352. .get("/api/ecs_oe_admin/exam/student/courseProgress/list", {
  353. params: {
  354. examId: this.examId,
  355. examStageId: this.examstageId,
  356. courseId: this.courseId
  357. }
  358. })
  359. .then(response => {
  360. if (response.data && response.data.length > 0) {
  361. this.courseProgressList = response.data;
  362. this.buildLine(response.data);
  363. } else {
  364. this.courseProgressList = [];
  365. this.lineOptions = {};
  366. }
  367. });
  368. },
  369. getCourses() {
  370. if (!this.examId) {
  371. return false;
  372. }
  373. this.$http
  374. .get("/api/ecs_oe_admin/exam/student/findCoursesByExamIdAndOrgId", {
  375. params: {
  376. examId: this.examId,
  377. examStageId: this.examStageId,
  378. orgId: this.orgId
  379. }
  380. })
  381. .then(response => {
  382. if (response.data && response.data.length > 0) {
  383. this.courseList = response.data;
  384. } else {
  385. this.courseList = [];
  386. }
  387. });
  388. },
  389. changeExam(examId) {
  390. var exam = this.examList.filter(item => {
  391. return item.id == examId;
  392. })[0];
  393. this.currentExamType = exam.examType;
  394. this.getPieData(this.currentExamType);
  395. this.getCourses();
  396. this.getOrgExamInfos();
  397. this.getCourseProgress();
  398. //场次联动
  399. if (this.examList.length > 0) {
  400. let examArr = this.examList.filter(p => p.id == examId);
  401. if (examArr && examArr.length > 0) {
  402. let exam = examArr[0];
  403. if (
  404. exam.specialSettingsEnabled &&
  405. exam.specialSettingsType == "STAGE_BASED"
  406. ) {
  407. this.examStageDisabled4Search = false;
  408. this.queryExamStages4Search("");
  409. } else {
  410. this.examStageList4Search = [];
  411. this.examStageDisabled4Search = true;
  412. }
  413. }
  414. }
  415. },
  416. changeExamStage() {
  417. this.getPieData(this.currentExamType);
  418. this.getCourses();
  419. this.getOrgExamInfos();
  420. this.getCourseProgress();
  421. },
  422. getPieData(examType) {
  423. var completedWord =
  424. examType == "ONLINE" || examType == "ONLINE_HOMEWORK"
  425. ? "已完成:"
  426. : "已抽题:";
  427. var noCompletedWord =
  428. examType == "ONLINE" || examType == "ONLINE_HOMEWORK"
  429. ? "未完成:"
  430. : "未抽题:";
  431. if (!this.examId) {
  432. return;
  433. }
  434. this.$http
  435. .post(
  436. "/api/ecs_oe_admin/exam/student/statistic/by/finished?examId=" +
  437. this.examId +
  438. "&examStageId=" +
  439. this.examStageId
  440. )
  441. .then(response => {
  442. var resp = response.data;
  443. var optionData = {
  444. title: "考试人次:" + (resp.finished + resp.unFinished),
  445. legendData: [
  446. noCompletedWord + resp.unFinished,
  447. completedWord + resp.finished
  448. ],
  449. seriesData: [
  450. {
  451. name: noCompletedWord + resp.unFinished,
  452. value: resp.unFinished
  453. },
  454. {
  455. name: completedWord + resp.finished,
  456. value: resp.finished
  457. }
  458. ]
  459. };
  460. this.buildPieOptions(optionData);
  461. });
  462. },
  463. buildPieOptions(data) {
  464. var colors = ["#7CB5EC", "#FE8463"];
  465. this.pieOptions = {
  466. color: colors,
  467. title: {
  468. text: data.title,
  469. subtext: "",
  470. x: "left"
  471. },
  472. tooltip: {
  473. trigger: "item",
  474. formatter: "{b}人次<br/>占比:{d}%"
  475. },
  476. legend: {
  477. type: "scroll",
  478. orient: "vertical",
  479. right: 200,
  480. top: 30,
  481. data: data.legendData
  482. },
  483. series: [
  484. {
  485. name: "",
  486. type: "pie",
  487. radius: "50%",
  488. center: ["35%", "60%"],
  489. data: data.seriesData,
  490. itemStyle: {
  491. emphasis: {
  492. shadowBlur: 10,
  493. shadowOffsetX: 0,
  494. shadowColor: "rgba(0, 0, 0, 0.5)"
  495. }
  496. }
  497. }
  498. ]
  499. };
  500. },
  501. buildLine(courseProgressList) {
  502. courseProgressList.sort(function(a, b) {
  503. if (b["completedProportion"] != a["completedProportion"]) {
  504. return b["completedProportion"] - a["completedProportion"];
  505. } else if (b["allNum"] != a["allNum"]) {
  506. return b["allNum"] - a["allNum"];
  507. } else {
  508. return b["completedNum"] - a["completedNum"];
  509. }
  510. });
  511. var campusCount = 5;
  512. var courseProgressDataList = [];
  513. //找出5个完成比例最高的
  514. if (courseProgressList.length >= campusCount) {
  515. courseProgressDataList = courseProgressList.slice(0, campusCount);
  516. } else {
  517. courseProgressDataList = courseProgressList;
  518. }
  519. var xAxisData = [];
  520. var seriesBar = {
  521. name: "计划数",
  522. type: "bar",
  523. data: []
  524. };
  525. var seriesLine1 = {
  526. name: "完成数",
  527. type: "line",
  528. data: []
  529. };
  530. var seriesLine2 = {
  531. name: "完成比(%)",
  532. type: "line",
  533. yAxisIndex: 1,
  534. data: []
  535. };
  536. var yAxis_maxScale1 = 0;
  537. for (var i = 0; i < courseProgressDataList.length; i++) {
  538. xAxisData.push(courseProgressDataList[i].courseName);
  539. seriesBar.data.push(courseProgressDataList[i].allNum);
  540. seriesLine1.data.push(courseProgressDataList[i].completedNum);
  541. seriesLine2.data.push(courseProgressDataList[i].completedProportion);
  542. if (courseProgressDataList[i].allNum > yAxis_maxScale1) {
  543. yAxis_maxScale1 = courseProgressDataList[i].allNum;
  544. }
  545. }
  546. var optionData = {
  547. legendData: ["计划数", "完成数", "完成比(%)"],
  548. xAxis: { data: xAxisData },
  549. series: [seriesBar, seriesLine1, seriesLine2],
  550. yAxis_maxScale1
  551. };
  552. this.buildLineOptions(optionData);
  553. },
  554. buildLineOptions(optionData) {
  555. var colors = ["#FE8463", "#66CCFF", "#675bba"];
  556. this.lineOptions = {
  557. color: colors,
  558. tooltip: {
  559. trigger: "axis",
  560. axisPointer: {
  561. type: "cross",
  562. crossStyle: {
  563. color: "#999"
  564. }
  565. }
  566. },
  567. toolbox: {
  568. feature: {
  569. dataView: { show: true, readOnly: false },
  570. magicType: { show: true, type: ["line", "bar"] },
  571. restore: { show: true },
  572. saveAsImage: { show: true }
  573. }
  574. },
  575. legend: {
  576. data: optionData.legendData
  577. },
  578. xAxis: [
  579. {
  580. type: "category",
  581. data: optionData.xAxis.data,
  582. axisPointer: {
  583. type: "shadow"
  584. }
  585. }
  586. ],
  587. yAxis: [
  588. {
  589. type: "value",
  590. name: "人次",
  591. min: 0,
  592. max: optionData.yAxis_maxScale1,
  593. interval: 10000,
  594. axisLabel: {
  595. formatter: "{value} "
  596. }
  597. },
  598. {
  599. type: "value",
  600. name: "完成比例",
  601. min: 0,
  602. max: 100,
  603. interval: 20,
  604. axisLabel: {
  605. formatter: "{value} %"
  606. }
  607. }
  608. ],
  609. series: optionData.series
  610. };
  611. },
  612. sortByFinishedPercent(obj1, obj2) {
  613. let p1 = Number(obj1.finishedPercent);
  614. let p2 = Number(obj2.finishedPercent);
  615. return p1 - p2;
  616. },
  617. exportOrg() {
  618. this.exportOrgLoading = true;
  619. this.$http
  620. .get("/api/ecs_oe_admin/exam/student/statistic/by/org/export", {
  621. params: {
  622. examId: this.examId,
  623. orgId: this.orgId
  624. },
  625. responseType: "arraybuffer",
  626. timeout: 20 * 60 * 1000 //限时20分钟
  627. })
  628. .then(response => {
  629. if (response.data) {
  630. var blob = new Blob([response.data], {
  631. type:
  632. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  633. });
  634. var url = URL.createObjectURL(blob);
  635. var a = document.createElement("a");
  636. a.href = url;
  637. a.download = "学习中心完成进度.xlsx";
  638. a.target = "_blank";
  639. a.click();
  640. URL.revokeObjectURL(url);
  641. }
  642. this.exportOrgLoading = false;
  643. });
  644. },
  645. exportCourse() {
  646. this.exportCourseLoading = true;
  647. this.$http
  648. .get("/api/ecs_oe_admin/exam/student/courseProgress/list/export", {
  649. params: {
  650. examId: this.examId,
  651. courseId: this.courseId
  652. },
  653. responseType: "arraybuffer",
  654. timeout: 20 * 60 * 1000 //限时20分钟
  655. })
  656. .then(response => {
  657. if (response.data) {
  658. var blob = new Blob([response.data], {
  659. type:
  660. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  661. });
  662. var url = URL.createObjectURL(blob);
  663. var a = document.createElement("a");
  664. a.href = url;
  665. a.download = "课程完成进度.xlsx";
  666. a.target = "_blank";
  667. a.click();
  668. URL.revokeObjectURL(url);
  669. }
  670. this.exportCourseLoading = false;
  671. });
  672. },
  673. refreshExamSyncPercentage() {
  674. if (!this.examId) {
  675. this.$notify({
  676. title: "警告",
  677. message: "请选择考试",
  678. type: "warning",
  679. duration: 2000
  680. });
  681. return false;
  682. }
  683. this.$http
  684. .get("/api/ecs_oe_admin/examControl/getExamSyncPercentage", {
  685. params: { examId: this.examId }
  686. })
  687. .then(response => {
  688. this.examSyncPercentage = response.data;
  689. })
  690. .catch(res => {
  691. var errorMsg = "操作失败";
  692. if (res.response && res.response.data) {
  693. errorMsg = res.response.data.desc;
  694. }
  695. this.$notify({
  696. title: "提示",
  697. message: errorMsg,
  698. type: "error"
  699. });
  700. });
  701. },
  702. queryExamStages4Search(name) {
  703. this.queryExamStages(this.examId, name, "search");
  704. },
  705. queryExamStages(examId, name, where) {
  706. debugger;
  707. console.log("queryExams; name: " + name);
  708. let url =
  709. EXAM_WORK_API +
  710. "/examStage/queryByNameLike?examId=" +
  711. examId +
  712. "&enable=true&name=" +
  713. name;
  714. this.$httpWithMsg
  715. .get(url)
  716. .then(response => {
  717. if ("search" == where) {
  718. this.queryExamStages4SearchLoading = false;
  719. this.examStageList4Search = response.data;
  720. }
  721. })
  722. .catch(response => {
  723. console.log(response);
  724. if ("search" == where) {
  725. this.queryExamStages4SearchLoading = false;
  726. }
  727. });
  728. }
  729. },
  730. created() {
  731. this.getExams();
  732. this.getOrgs();
  733. this.refreshExamSyncPercentage();
  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>