Browse Source

阅卷设置

zhangjie 1 year ago
parent
commit
1f3cd7ad64

+ 97 - 0
src/modules/mark/components/SelectFile.vue

@@ -0,0 +1,97 @@
+<template>
+  <div class="select-file" style="display: inline-block">
+    <el-input
+      :style="{ width: inputWidth }"
+      v-model.trim="attachmentName"
+      :placeholder="placeholder"
+      readonly
+    ></el-input>
+    <el-upload
+      :action="uploadUrl"
+      :before-upload="() => false"
+      :show-file-list="false"
+      :disabled="disabled"
+      :on-change="fileChange"
+      style="display: inline-block; margin: 0 10px"
+    >
+      <el-button type="primary" :disabled="disabled">选择</el-button>
+    </el-upload>
+  </div>
+</template>
+
+<script>
+import { fileMD5 } from "@/plugins/md5";
+
+export default {
+  name: "select-file",
+  props: {
+    inputWidth: {
+      type: String,
+      default: "340px",
+    },
+    placeholder: {
+      type: String,
+      default: "请选择文件",
+    },
+    format: {
+      type: Array,
+      default() {
+        return ["pdf"];
+      },
+    },
+    maxSize: {
+      type: Number,
+      default: 20 * 1024 * 1024,
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      attachmentName: "",
+      uploadUrl: "",
+      errorMsg: "",
+    };
+  },
+  methods: {
+    checkFileFormat(fileType) {
+      const _file_format = fileType.split(".").pop().toLocaleLowerCase();
+      if (!this.format.length) return true;
+
+      return this.format.some(
+        (item) => item.toLocaleLowerCase() === _file_format
+      );
+    },
+    async fileChange(file) {
+      console.log(file);
+      this.errorMsg = "";
+      this.attachmentName = "";
+
+      if (file.size > this.maxSize) {
+        this.errorMsg =
+          "文件大小不能超过" + Math.floor(this.maxSize / 1024) + "M";
+        this.$emit("file-change", { errorMsg: this.errorMsg });
+        return;
+      }
+
+      if (!this.checkFileFormat(file.name)) {
+        this.errorMsg = "只支持文件格式为" + this.format.join("/");
+        this.$emit("file-change", { errorMsg: this.errorMsg });
+        return;
+      }
+
+      this.attachmentName = file.name;
+      const md5 = await fileMD5(file.raw);
+      this.$emit("file-change", {
+        file: file.raw,
+        md5,
+      });
+    },
+    setAttachmentName(name) {
+      this.attachmentName = name;
+    },
+  },
+};
+</script>

+ 169 - 0
src/modules/mark/components/markParam/MarkParamClass.vue

@@ -0,0 +1,169 @@
+<template>
+  <div class="mark-param-class">
+    <div class="box-justify part-box part-box-pad">
+      <div></div>
+      <div>
+        <el-button type="primary" @click="toImport">导入</el-button>
+      </div>
+    </div>
+
+    <div class="part-box part-box-pad">
+      <el-table :data="classList" border>
+        <el-table-column type="index" width="50"> </el-table-column>
+
+        <el-table-column label="班级">
+          <template slot-scope="scope">
+            <el-tag size="medium" type="info">
+              {{ scope.row.className }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="评阅题目" prop="groupQuestions" width="200">
+        </el-table-column>
+        <el-table-column label="评卷员" width="200">
+          <template slot-scope="scope">
+            <el-tag
+              v-for="item in scope.row.classMarkerList"
+              :key="item"
+              size="medium"
+              type="info"
+              class="mb-1 mr-1"
+            >
+              {{ scope.row.name }}({{ scope.row.orgName }})
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column class-name="action-column" label="操作" width="120">
+          <template slot-scope="scope">
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toSelectMarker(scope.row)"
+              >选择评卷员</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 选择评卷员 -->
+    <el-dialog
+      :visible.sync="modalIsShow"
+      title="选择评卷员"
+      top="10px"
+      width="600px"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      append-to-body
+    >
+      <el-form ref="modalFormComp" :model="modalForm" :rules="rules">
+        <el-form-item prop="selectedMarkerIds">
+          <el-checkbox-group v-model="modalForm.selectedMarkerIds">
+            <el-checkbox
+              v-for="mark in curClass.markerList"
+              :key="mark.userId"
+              :label="mark.userId"
+            >
+              {{ mark.name }}({{ mark.orgName }})
+            </el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button type="primary" :disabled="isSubmit" @click="conform"
+          >确认</el-button
+        >
+        <el-button @click="modalIsShow = false">取消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { markClassList, markClassSave } from "../../api";
+import { mapState } from "vuex";
+
+export default {
+  name: "mark-param-class",
+  components: {},
+  data() {
+    return {
+      classList: [],
+      curClass: {},
+      // modify
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { selectedMarkerIds: [] },
+      rules: {
+        selectedMarkerIds: [
+          {
+            required: true,
+            validator: (rule, value, callback) => {
+              if (!value.length) {
+                callback(new Error("请选择评卷员"));
+              } else {
+                callback();
+              }
+            },
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  computed: {
+    ...mapState("markParam", ["basicInfo", "paperStructureInfo"]),
+  },
+  mounted() {
+    this.initData();
+  },
+  methods: {
+    async initData() {
+      const params = {
+        examId: this.basicInfo.examId,
+        paperNumber: this.basicInfo.paperNumber,
+      };
+      const res = await markClassList(params);
+      this.classList = res || [];
+    },
+    toSelectMarker(row) {
+      this.curClass = row;
+      this.modalIsShow = true;
+    },
+    toImport() {
+      // TODO:
+    },
+    async conform() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      this.isSubmit = true;
+      const classMarkerList = this.curClass.markerList.filter((item) =>
+        this.modalForm.selectedMarkerIds.includes(item.userId)
+      );
+      const res = await markClassSave({
+        examId: this.basicInfo.examId,
+        paperNumber: this.basicInfo.paperNumber,
+        className: this.curClass.className,
+        groupNumber: this.curClass.groupNumber,
+        classMarkerList,
+      }).catch(() => {});
+      this.isSubmit = false;
+      if (!res) return;
+      this.$message.success("操作成功!");
+      this.curClass.classMarkerList = classMarkerList;
+      this.modalIsShow = false;
+    },
+    getData() {
+      return {
+        openClassReading: this.openClassReading,
+        classInfo: this.markerClassList.map((item) => {
+          let nitem = { ...item };
+          nitem.className = item.className.join();
+          return nitem;
+        }),
+      };
+    },
+  },
+};
+</script>

+ 257 - 0
src/modules/mark/components/markParam/MarkParamObjectiveAnswer.vue

@@ -0,0 +1,257 @@
+<template>
+  <div class="mark-param-objective-answer">
+    <div class="part-box part-box-pad">
+      <div>
+        <p class="tips-info">1.请录入客观题标答;</p>
+        <p class="tips-info">
+          2.多选题请根据需求设置判分策略,目前支持漏选给半分,任选给半分或者不设置,不设置时表示与标答一致得分,否则不得分。
+        </p>
+      </div>
+    </div>
+
+    <div class="part-box part-box-pad">
+      <p class="tips-info mb-1">
+        <i class="el-icon-warning"></i> 客观题标答只能输入大写字母ABCDE...
+        ,判断题正确请输入A,错误输入B
+      </p>
+      <el-table
+        ref="TableList"
+        :data="tableData"
+        border
+        :row-class-name="getRowClassName"
+      >
+        <el-table-column width="50" align="center">
+          <template slot-scope="scope" v-if="scope.row.mainFirstSub">
+            <div
+              :class="[
+                'expand-btn',
+                { 'expand-btn-unexpand': !scope.row.expandSub },
+              ]"
+              @click="switchExpandSub(scope.row)"
+            >
+              <i
+                :class="scope.row.expandSub ? 'el-icon-minus' : 'el-icon-plus'"
+              ></i>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="mainTitle" label="大题名称">
+          <span slot-scope="scope" v-if="scope.row.mainFirstSub">
+            {{ scope.row.mainTitle }}
+          </span>
+        </el-table-column>
+        <el-table-column prop="mainNumber" label="大题号" width="80">
+          <template slot-scope="scope" v-if="scope.row.mainFirstSub">
+            <span>{{ scope.row.mainNumber }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="subNumber"
+          label="小题号"
+          width="80"
+        ></el-table-column>
+        <el-table-column prop="totalScore" label="小题满分" width="105">
+        </el-table-column>
+        <el-table-column label="答案" width="200px" class-name="answer-column">
+          <div
+            slot-scope="scope"
+            :class="['el-form-item', { 'is-error': scope.row.error }]"
+          >
+            <div class="el-form-item__content">
+              <el-input
+                v-model.trim="scope.row.answer"
+                :placeholder="
+                  scope.row.type === 3 ? 'A:正确,B:错误' : '请输入答案'
+                "
+                :maxlength="scope.row.optionCount"
+                clearable
+                @change="validateAnswer(scope.row)"
+              ></el-input>
+              <div v-if="scope.row.error" class="el-form-item__error">
+                {{ scope.row.errMsg }}
+              </div>
+            </div>
+          </div>
+        </el-table-column>
+        <el-table-column label="判分策略" width="140px">
+          <template v-if="scope.row.type === 2" slot-scope="scope">
+            <el-select v-model="scope.row.objectivePolicy">
+              <el-option
+                v-for="(val, key) in QUESTION_SCORE_TYPE"
+                :key="key"
+                :value="key"
+                :label="val"
+              ></el-option>
+            </el-select>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <div class="text-center">
+      <el-button type="primary" :disabled="loading" @click="submit"
+        >提交</el-button
+      >
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { QUESTION_SCORE_TYPE } from "@/constants/enumerate";
+import {
+  markObjectiveQuestionList,
+  markObjectiveQuestionSave,
+} from "../../api";
+import { mapState } from "vuex";
+import { omit } from "lodash";
+
+export default {
+  name: "mark-param-objective-answer",
+  data() {
+    return {
+      loading: false,
+      tableData: [],
+      QUESTION_SCORE_TYPE,
+      abc: "abcdefghijklmnopqrstuvwxyz".toUpperCase(),
+    };
+  },
+  computed: {
+    ...mapState("markParam", ["basicInfo"]),
+  },
+  mounted() {
+    this.initData();
+  },
+  methods: {
+    async initData() {
+      const params = {
+        examId: this.basicInfo.examId,
+        paperNumber: this.basicInfo.paperNumber,
+      };
+      const res = await markObjectiveQuestionList(params);
+      let objectiveStructure = res || [];
+      let curMainNumber = null;
+      let curMainId = null;
+      this.tableData = objectiveStructure.map((item) => {
+        let nitem = {
+          ...item,
+          id: this.$randomCode(),
+          mainFirstSub: false,
+          expandSub: true,
+          error: false,
+          errMsg: "",
+        };
+        if (nitem.mainNumber !== curMainNumber) {
+          curMainNumber = nitem.mainNumber;
+          nitem.mainFirstSub = true;
+          curMainId = this.$randomCode();
+        }
+        nitem.mainId = curMainId;
+        return nitem;
+      });
+    },
+    getRowClassName({ row }) {
+      let classNames = [];
+      if (row.mainFirstSub) {
+        classNames.push("row-main-first-sub");
+      }
+      if (!row.mainFirstSub && !row.expandSub) {
+        classNames.push("row-unexpand-sub");
+      }
+      return classNames.join(" ");
+    },
+    switchExpandSub(row) {
+      row.expandSub = !row.expandSub;
+      this.tableData
+        .filter((item) => item.mainId === row.mainId && !item.mainFirstSub)
+        .forEach((item) => (item.expandSub = row.expandSub));
+    },
+    validateAnswer(row) {
+      if (!row.answer) {
+        row.error = true;
+        row.errMsg = "不能为空";
+        return;
+      }
+
+      if (!/^[A-Z]{1,26}$/.test(row.answer)) {
+        row.error = true;
+        row.errMsg = "只能输入英文大写字母";
+        return;
+      }
+
+      const validAnswers = this.abc.substring(0, row.optionCount);
+      // 单选题 判断题
+      if (row.questionType === 1 || row.questionType === 3) {
+        if (
+          row.answer.length !== 1 ||
+          row.answer.split("").some((item) => !validAnswers.includes(item))
+        ) {
+          row.error = true;
+          row.errMsg = `只能输入${validAnswers}中的一个`;
+          return;
+        }
+      }
+
+      // 多选题
+      if (row.questionType === 2) {
+        if (row.answer.length <= 1) {
+          row.error = true;
+          row.errMsg = `答案必须为多个`;
+          return;
+        }
+        if (row.answer.split("").some((item) => !validAnswers.includes(item))) {
+          row.error = true;
+          row.errMsg = `只能输入${validAnswers}中的字符`;
+          return;
+        }
+
+        const ansSet = new Set(row.answer.split(""));
+        if (ansSet.size !== row.answer.length) {
+          row.error = true;
+          row.errMsg = "答案不能重复";
+          return;
+        }
+      }
+
+      row.error = false;
+      row.errMsg = "";
+    },
+    checkData() {
+      this.tableData.forEach((item) => {
+        this.validateAnswer(item);
+      });
+
+      return !this.tableData.some((item) => item.error);
+    },
+    async submit() {
+      if (!this.checkData()) return;
+
+      if (this.loading) return;
+      this.loading = true;
+      const datas = {
+        examId: this.basicInfo.examId,
+        paperNumber: this.basicInfo.paperNumber,
+        objectiveInfo: this.tableData.map((item) =>
+          omit(item, [
+            "id",
+            "mainFirstSub",
+            "expandSub",
+            "error",
+            "errMsg",
+            "mainId",
+          ])
+        ),
+      };
+      const data = await markObjectiveQuestionSave(datas).catch(() => {});
+      this.loading = false;
+      if (!data) return;
+
+      this.$message.success("保存成功!");
+      // this.$emit("confirm");
+    },
+    cancel() {
+      this.$emit("cancel");
+    },
+  },
+};
+</script>

+ 4 - 1
src/modules/mark/components/markParam/MarkParamStructure.vue

@@ -217,6 +217,7 @@ export default {
       this.questionTypeDict = questionTypeDict;
 
       let curMainNumber = null;
+      let curMainId = null;
       let scoresPerTopic = {};
       this.tableData = this.paperStructureInfo.map((item) => {
         let nitem = {
@@ -227,10 +228,12 @@ export default {
         };
         if (curMainNumber !== item.mainNumber) {
           curMainNumber = item.mainNumber;
+          curMainId = this.$randomCode();
           scoresPerTopic[curMainNumber] = undefined;
           nitem.mainFirstSub = true;
-          nitem.mainId = this.$randomCode();
         }
+        nitem.mainId = curMainId;
+
         return nitem;
       });
       this.scoresPerTopic = scoresPerTopic;

+ 122 - 0
src/modules/mark/components/markParam/MarkParamSubjectiveAnswer.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="mark-param-subjective-answer">
+    <div class="part-box part-box-pad">
+      <div>
+        <p class="tips-info">1.主观题标答请上传PDF文档;</p>
+        <p class="tips-info">
+          2.主观题标答文档上传后,在评卷界面小助手里可以打开进行查看,作为评卷参考;
+        </p>
+        <p class="tips-info">3.支持重复提交,以最后一次提交标答文件为准。</p>
+      </div>
+    </div>
+    <div class="part-box part-box-pad">
+      <el-form ref="modalFormComp" :model="infos" label-width="50px">
+        <div class="part-box">
+          <h3 class="mb-2">卷型{{ basicInfo.paperType }}</h3>
+          <el-form-item prop="file" label="标答:">
+            <select-file
+              :format="fileFormat"
+              :disabled="loading"
+              @file-change="fileChange"
+            ></select-file>
+            <p v-if="infos.errorMsg" class="tips-info tips-error">
+              {{ infos.errorMsg }}
+            </p>
+          </el-form-item>
+          <el-form-item v-if="answerFileUrl">
+            <span
+              class="cont-link"
+              title="点击查看已上传标答文件"
+              @click="toViewAnswer"
+            >
+              <i class="el-icon-document mr-1"></i>{{ answerFileName }}
+            </span>
+          </el-form-item>
+        </div>
+      </el-form>
+    </div>
+    <div class="text-center">
+      <el-button type="primary" :disabled="loading" @click="submit"
+        >确认</el-button
+      >
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import {
+  markSubjectiveQuestionList,
+  markSubjectiveQuestionUpload,
+} from "../../api";
+import SelectFile from "../SelectFile.vue";
+
+export default {
+  name: "mark-param-subjective-answer",
+  components: { SelectFile },
+  data() {
+    return {
+      modalIsShow: false,
+      loading: false,
+      infos: { file: null, md5: null, errorMsg: null },
+      fileFormat: ["pdf"],
+      answerFileUrl: "",
+    };
+  },
+  computed: {
+    ...mapState("markParam", ["basicInfo"]),
+    answerFileName() {
+      return `${this.basicInfo.courseName}-标答文件`;
+    },
+  },
+  methods: {
+    async initData() {
+      const params = {
+        examId: this.basicInfo.examId,
+        paperNumber: this.basicInfo.paperNumber,
+      };
+      const res = await markSubjectiveQuestionList(params);
+      this.answerFileUrl = res || "";
+    },
+    toViewAnswer() {
+      window.open(this.answerFileUrl);
+    },
+    fileChange(data) {
+      if (data.errorMsg) {
+        this.infos.file = null;
+        this.infos.md5 = null;
+        this.infos.errorMsg = data.errorMsg;
+      } else {
+        this.infos.file = data.file;
+        this.infos.md5 = data.md5;
+        this.infos.errorMsg = null;
+      }
+    },
+    async submit() {
+      if (!this.infos.file) {
+        this.$message.error("请选择标答文件");
+        return;
+      }
+
+      if (this.loading) return;
+      this.loading = true;
+
+      let formData = new FormData();
+      formData.append("examId", this.basicInfo.examId);
+      formData.append("paperNumber", this.basicInfo.paperNumber);
+      formData.append(`file`, this.infos.file);
+      formData.append(`md5`, this.infos.md5);
+      const data = await markSubjectiveQuestionUpload(formData).catch(() => {});
+      this.loading = false;
+      if (!data) return;
+
+      this.$message.success("上传成功!");
+      // this.$emit("confirm");
+    },
+    cancel() {
+      this.$emit("cancel");
+    },
+  },
+};
+</script>

+ 8 - 8
src/modules/mark/components/markParam/ModifyMarkParams.vue

@@ -45,20 +45,20 @@
 <script>
 import { mapState, mapMutations } from "vuex";
 import MarkParamStructure from "./MarkParamStructure.vue";
-// import MarkParamGroup from "./MarkParamGroup.vue";
-// import MarkParamClass from "./MarkParamClass.vue";
-// import MarkParamObjectiveAnswer from "./MarkParamObjectiveAnswer.vue";
-// import MarkParamSubjectiveAnswer from "./MarkParamSubjectiveAnswer.vue";
+import MarkParamGroup from "./MarkParamGroup.vue";
+import MarkParamClass from "./MarkParamClass.vue";
+import MarkParamObjectiveAnswer from "./MarkParamObjectiveAnswer.vue";
+import MarkParamSubjectiveAnswer from "./MarkParamSubjectiveAnswer.vue";
 import { markStructureList, markGroupList } from "../../api";
 
 export default {
   name: "modify-mark-params",
   components: {
     MarkParamStructure,
-    // MarkParamGroup,
-    // MarkParamClass,
-    // MarkParamObjectiveAnswer,
-    // MarkParamSubjectiveAnswer,
+    MarkParamGroup,
+    MarkParamClass,
+    MarkParamObjectiveAnswer,
+    MarkParamSubjectiveAnswer,
   },
   props: {
     instance: {