Browse Source

阅卷管理-评卷质量

zhangjie 1 year ago
parent
commit
0d51c05a18

+ 2 - 0
package.json

@@ -14,12 +14,14 @@
     "cropperjs": "^1.5.1",
     "crypto-js": "^4.0.0",
     "deepmerge": "^4.2.2",
+    "echarts": "^5.4.3",
     "element-ui": "^2.14.1",
     "js-md5": "^0.7.3",
     "jsbarcode": "^3.11.3",
     "lodash": "^4.17.21",
     "qs": "^6.11.0",
     "vue": "^2.6.14",
+    "vue-echarts": "^6.6.1",
     "vue-ls": "^3.2.2",
     "vue-router": "^3.5.1",
     "vuex": "^3.6.2"

+ 10 - 0
src/modules/mark/api.js

@@ -111,3 +111,13 @@ export const markMarkerRecycle = (datas) => {
 export const markMarkerSetTaskCount = (datas) => {
   return $postParam("/api/admin/mark/marker/setTaskCount", datas);
 };
+// mark-detail-quality
+export const markQualityListPage = (datas) => {
+  return $postParam("/api/admin/mark/quality/list", datas);
+};
+export const markQualityUpdate = (datas) => {
+  return $postParam("/api/admin/mark/quality/update", datas);
+};
+export const markQualityChart = (datas) => {
+  return $postParam("/api/admin/mark/quality/chart", datas);
+};

+ 186 - 0
src/modules/mark/components/markDetail/MarkDetailQuality.vue

@@ -0,0 +1,186 @@
+<template>
+  <div class="mark-detail-quality">
+    <div class="part-box part-box-filter part-box-flex">
+      <el-form ref="FilterForm" label-position="left" label-width="85px" inline>
+        <el-form-item label="评阅题目">
+          <el-select
+            v-model="filter.groupQuestion"
+            placeholder="评阅题目"
+            clearable
+          >
+            <el-option :value="1">班级1</el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="评卷员">
+          <el-input
+            v-model.trim="filter.loginName"
+            placeholder="评卷员"
+            clearable
+          >
+          </el-input>
+        </el-form-item>
+        <el-form-item label-width="0px">
+          <el-button type="primary" @click="search">查询</el-button>
+        </el-form-item>
+      </el-form>
+      <div class="part-box-action">
+        <el-button type="primary" @click="toReset"> 重新计算 </el-button>
+        <el-button type="primary" @click="toViewLine"> 给分曲线 </el-button>
+      </div>
+    </div>
+
+    <div class="part-box part-box-pad">
+      <el-table
+        ref="TableList"
+        :data="dataList"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column
+          type="selection"
+          width="55"
+          align="center"
+        ></el-table-column>
+        <el-table-column prop="courseName" label="评卷员" min-width="100">
+          <template slot-scope="scope">
+            <el-tag size="medium" type="info">
+              {{ scope.row.name }}({{ scope.row.orgName }})
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="groupQuestions"
+          label="评阅题目"
+          min-width="200"
+        ></el-table-column>
+        <el-table-column
+          prop="finishCount"
+          label="完成任务数"
+          width="100"
+        ></el-table-column>
+        <el-table-column
+          prop="rejectCount"
+          label="打回次数"
+          width="100"
+        ></el-table-column>
+        <el-table-column prop="adoptionRate" label="评卷采用率" width="100">
+          <span slot-scope="scope">{{ scope.row.adoptionRate || 0 }}%</span>
+        </el-table-column>
+        <el-table-column
+          prop="avgSpeed"
+          label="评卷速度(秒)"
+          width="100"
+        ></el-table-column>
+        <el-table-column
+          prop="avgScore"
+          label="平均分"
+          width="100"
+        ></el-table-column>
+        <el-table-column
+          prop="maxScore"
+          label="最高分"
+          width="100"
+        ></el-table-column>
+        <el-table-column
+          prop="minScore"
+          label="最低分"
+          width="100"
+        ></el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          background
+          layout="total, sizes, prev, pager, next, jumper"
+          :pager-count="5"
+          :current-page="current"
+          :total="total"
+          :page-size="size"
+          @current-change="toPage"
+          @size-change="pageSizeChange"
+        >
+        </el-pagination>
+      </div>
+    </div>
+    <!-- QualityChartDialog -->
+    <quality-chart-dialog
+      ref="QualityChartDialog"
+      :data="chartData"
+    ></quality-chart-dialog>
+  </div>
+</template>
+
+<script>
+import { markQualityListPage, markQualityUpdate } from "../api";
+import QualityChartDialog from "./QualityChartDialog.vue";
+
+export default {
+  name: "mark-detail-quality",
+  props: {
+    baseInfo: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  components: { QualityChartDialog },
+  data() {
+    return {
+      filter: {
+        groupQuestion: "",
+        loginName: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      multipleSelection: [],
+      chartData: {},
+    };
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await markQualityListPage(datas);
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    search() {
+      this.toPage(1);
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val.map((item) => item.markUserGroupId);
+    },
+    async toReset(row) {
+      const confirm = await this.$confirm("确定要重新计算吗?", "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await markQualityUpdate({
+        examId: this.baseInfo.examId,
+        paperNumber: this.baseInfo.paperNumber,
+        // groupNumber
+      });
+      this.$message.success("操作成功!");
+      this.getList();
+    },
+    toViewLine() {
+      // TODO: 评卷员,阅卷题目必选
+      this.chartData = {
+        examId: this.baseInfo.examId,
+        paperNumber: this.baseInfo.paperNumber,
+        // groupNumber
+      };
+      this.$refs.QualityChartDialog.open();
+    },
+  },
+};
+</script>

+ 189 - 0
src/modules/mark/components/markDetail/QualityChartDialog.vue

@@ -0,0 +1,189 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    title="给分曲线"
+    top="10vh"
+    width="800px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @opened="visibleChange"
+  >
+    <p>
+      <span>全体平均</span>
+      <span>最高分:{{ summary.max }}</span>
+      <span>最低分:{{ summary.min }}</span>
+      <span>平均分:{{ summary.avg }}</span>
+    </p>
+    <div class="chart-content">
+      <v-chart v-if="chartOption" :option="chartOption"></v-chart>
+    </div>
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import { calcAvg } from "@/plugins/utils";
+import { markQualityChart } from "../api";
+
+export default {
+  name: "quality-chart-dialog",
+  props: {
+    data: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      chartOption: null,
+      summary: {
+        avg: 0,
+        max: 0,
+        min: 0,
+      },
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.initData();
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async initData() {
+      this.chartOption = null;
+      this.summary = {
+        avg: 0,
+        max: 0,
+        min: 0,
+      };
+      const res = await markQualityChart(this.data);
+      const datalist = res || [];
+      this.chartOption = this.getChartOption(datalist);
+      this.summary = {
+        avg: calcAvg(datalist.map((item) => item.avgScore)).toFixed(2),
+        min: calcAvg(datalist.map((item) => item.minScore)).toFixed(2),
+        max: calcAvg(datalist.map((item) => item.maxScore)).toFixed(2),
+      };
+    },
+    getChartOption(dataList) {
+      let option = {
+        tooltip: {
+          trigger: "axis",
+          axisPointer: {
+            type: "shadow",
+          },
+        },
+        grid: {
+          top: "10%",
+          bottom: "20%",
+          left: "3%",
+          right: "3%",
+        },
+        legend: {
+          data: ["最高分", "平均分", "最低分"],
+          top: 10,
+          right: 10,
+          itemWidth: 14,
+          itemGap: 22,
+          textStyle: {
+            color: "#626A82",
+            fontSize: 12,
+          },
+        },
+        xAxis: [
+          {
+            type: "category",
+            name: "评卷员",
+            data: dataList.map((item) => item.name),
+            axisLine: {
+              show: true,
+              lineStyle: {
+                color: "#C1CBDB",
+              },
+            },
+            splitLine: {
+              show: false,
+            },
+            axisTick: {
+              show: false,
+            },
+            axisLabel: {
+              show: false,
+            },
+          },
+        ],
+        yAxis: [
+          {
+            type: "value",
+            name: "分值",
+            axisLine: {
+              show: false,
+            },
+            splitLine: {
+              show: true,
+              lineStyle: {
+                color: "#C1CBDB",
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            axisLabel: {
+              color: "#626A82",
+              fontSize: 12,
+            },
+          },
+        ],
+        series: [
+          {
+            name: "最高分",
+            type: "bar",
+            barGap: 0,
+            barMaxWidth: 10,
+            itemStyle: {
+              color: "#FE5863",
+            },
+            data: dataList.map((item) => item.maxScore),
+          },
+          {
+            name: "平均分",
+            type: "bar",
+            barGap: 0,
+            barMaxWidth: 10,
+            itemStyle: {
+              color: "#FECA57",
+            },
+            data: dataList.map((item) => item.avgScore),
+          },
+          {
+            name: "最低分",
+            type: "bar",
+            barGap: 0,
+            barMaxWidth: 10,
+            itemStyle: {
+              color: "#5FC9FA",
+            },
+            data: dataList.map((item) => item.minScore),
+          },
+        ],
+      };
+
+      return option;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.chart-content {
+  height: 400px;
+}
+</style>

+ 45 - 0
src/plugins/VueCharts.js

@@ -0,0 +1,45 @@
+import Vue from "vue";
+import ECharts from "vue-echarts";
+import { use } from "echarts/core";
+
+// import ECharts modules manually to reduce bundle size
+// 按需模块:https://github.com/apache/echarts/blob/master/src/echarts.all.ts
+// render type
+import { CanvasRenderer } from "echarts/renderers";
+
+// charts
+import {
+  BarChart,
+  LineChart,
+  // PieChart,
+  // ScatterChart,
+  // RadarChart,
+  BoxplotChart,
+} from "echarts/charts";
+
+// component
+import {
+  TitleComponent,
+  LegendComponent,
+  DataZoomComponent,
+  MarkLineComponent,
+  TooltipComponent,
+} from "echarts/components";
+
+use([
+  CanvasRenderer,
+  BarChart,
+  LineChart,
+  // PieChart,
+  // ScatterChart,
+  // RadarChart,
+  BoxplotChart,
+  TitleComponent,
+  LegendComponent,
+  DataZoomComponent,
+  MarkLineComponent,
+  TooltipComponent,
+]);
+
+// register component to use
+Vue.component("v-chart", ECharts);

+ 9 - 0
src/plugins/utils.js

@@ -330,6 +330,15 @@ export function calcSum(dataList) {
   }, 0);
 }
 
+/**
+ * 计算评卷数
+ * @param {Array} dataList 需要统计的数组
+ */
+export function calcAvg(dataList) {
+  if (!dataList.length) return 0;
+  return calcSum(dataList) / dataList.length;
+}
+
 /** 获取数组最大数 */
 export function maxNum(dataList) {
   return Math.max.apply(null, dataList);

+ 38 - 0
yarn.lock

@@ -2801,6 +2801,14 @@ easy-stack@1.0.1:
   resolved "https://registry.npmmirror.com/easy-stack/-/easy-stack-1.0.1.tgz#8afe4264626988cabb11f3c704ccd0c835411066"
   integrity sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==
 
+echarts@^5.4.3:
+  version "5.4.3"
+  resolved "https://registry.npmmirror.com/echarts/-/echarts-5.4.3.tgz#f5522ef24419164903eedcfd2b506c6fc91fb20c"
+  integrity sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==
+  dependencies:
+    tslib "2.3.0"
+    zrender "5.4.4"
+
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -5226,6 +5234,11 @@ requires-port@^1.0.0:
   resolved "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
 
+resize-detector@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.npmmirror.com/resize-detector/-/resize-detector-0.3.0.tgz#fe495112e184695500a8f51e0389f15774cb1cfc"
+  integrity sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==
+
 resize-observer-polyfill@^1.5.0:
   version "1.5.1"
   resolved "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
@@ -5880,6 +5893,11 @@ tr46@~0.0.3:
   resolved "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
   integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
 
+tslib@2.3.0:
+  version "2.3.0"
+  resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
+  integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
+
 tslib@^2.0.3, tslib@^2.1.0:
   version "2.5.0"
   resolved "https://registry.npmmirror.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
@@ -6006,6 +6024,19 @@ vary@~1.1.2:
   resolved "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
 
+vue-demi@^0.13.11:
+  version "0.13.11"
+  resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99"
+  integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==
+
+vue-echarts@^6.6.1:
+  version "6.6.1"
+  resolved "https://registry.npmmirror.com/vue-echarts/-/vue-echarts-6.6.1.tgz#5b0398427ea98ba33175bdc80f34af4ce8f27ee4"
+  integrity sha512-EpreTzlNeJ+eaUn0AhXEmKJk98xJGecgTqAdyZovoXWnhTxnlW2HuBM0ei3y8rLw1JCUabf8/sYvxjlr8SzBKQ==
+  dependencies:
+    resize-detector "^0.3.0"
+    vue-demi "^0.13.11"
+
 vue-eslint-parser@^8.0.1:
   version "8.3.0"
   resolved "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz#5d31129a1b3dd89c0069ca0a1c88f970c360bd0d"
@@ -6369,3 +6400,10 @@ yorkie@^2.0.0:
     is-ci "^1.0.10"
     normalize-path "^1.0.0"
     strip-indent "^2.0.0"
+
+zrender@5.4.4:
+  version "5.4.4"
+  resolved "https://registry.npmmirror.com/zrender/-/zrender-5.4.4.tgz#8854f1d95ecc82cf8912f5a11f86657cb8c9e261"
+  integrity sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==
+  dependencies:
+    tslib "2.3.0"