examSummary.vue 23 KB

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