Explorar el Código

试卷结构,评卷任务

zhangjie hace 1 año
padre
commit
0e59be7681

+ 2 - 1
package.json

@@ -17,6 +17,7 @@
     "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-ls": "^3.2.2",
@@ -50,4 +51,4 @@
       "git add"
     ]
   }
-}
+}

+ 5 - 0
src/constants/enumerate.js

@@ -359,3 +359,8 @@ export const MARK_MODE_TYPE = {
   TRACK: "轨迹模式",
   COMMON: "普通模式",
 };
+export const SCORE_POLICY_TYPE = {
+  AVG: "平均分",
+  MAX: "最高分",
+  MIN: "最低分",
+};

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

@@ -15,3 +15,54 @@ export const markSettingDetail = (datas) => {
 export const markSettingSave = (datas) => {
   return $post("/api/admin/mark/setting/paper/save", datas);
 };
+// mark-params
+// structure
+export const markStructureList = (datas) => {
+  return $postParam("/api/admin/mark/question/list", datas);
+};
+export const markStructureSave = (datas) => {
+  return $post("/api/admin/mark/question/save", datas);
+};
+// group
+export const markGroupList = (datas) => {
+  return $postParam("/api/admin/mark/group/list", datas);
+};
+export const markGroupItemSave = (datas) => {
+  return $post("/api/admin/mark/group/save", datas);
+};
+export const markGroupItemUpdate = (datas) => {
+  return $post("/api/admin/mark/group/update", datas);
+};
+export const markGroupAreaSave = (datas) => {
+  return $post("/api/admin/mark/group/update_picture_config", datas);
+};
+export const markGroupItemDelete = (datas) => {
+  return $postParam("/api/admin/mark/group/delete", datas);
+};
+// class
+export const markClassStatusUpdate = (datas) => {
+  return $postParam(
+    "/api/admin/mark/group/update_open_mark_class_status",
+    datas
+  );
+};
+export const markClassList = (datas) => {
+  return $postParam("/api/admin/mark/class/list", datas);
+};
+export const markClassSave = (datas) => {
+  return $post("/api/admin/mark/class/save", datas);
+};
+// objective question answer
+export const markObjectiveQuestionList = (datas) => {
+  return $postParam("/api/admin/mark/question/objective/list", datas);
+};
+export const markObjectiveQuestionSave = (datas) => {
+  return $post("/api/admin/mark/question/objective/save", datas);
+};
+// subjective question answer
+export const markSubjectiveQuestionList = (datas) => {
+  return $postParam("/api/admin/mark/question/subjective/get", datas);
+};
+export const markSubjectiveQuestionUpload = (datas) => {
+  return $post("/api/admin/mark/question/subjective/upload", datas);
+};

+ 1 - 1
src/modules/mark/components/ModifyMarkSetting.vue

@@ -16,7 +16,7 @@
       :model="modalForm"
       :rules="rules"
       :key="modalForm.paperNumber"
-      label-position="top"
+      label-width="140px"
     >
       <el-form-item prop="markMode" label="评卷模式:">
         <el-select v-model="modalForm.markMode">

+ 480 - 0
src/modules/mark/components/markParam/MarkParamGroup.vue

@@ -0,0 +1,480 @@
+<template>
+  <div class="mark-param-group">
+    <div class="box-justify part-box part-box-pad">
+      <div>
+        <p class="tips-info">
+          1.如果采用整卷批阅,请将全部主观题选上。如果采用分题阅卷,请将需要在一个评阅任务里评阅的题目勾选在一起;
+        </p>
+        <p class="tips-info">
+          2.如果采用分班阅卷,每个班都将按设置的评卷任务进行评卷,请给每个班级选择对应评卷老师;
+        </p>
+        <p class="tips-info tips-error">3.开始阅卷后不允许删除评卷分组!</p>
+      </div>
+      <div>
+        <span>分班阅卷</span>
+        <el-switch
+          v-model="markClassIsOpen"
+          @change="markClassChange"
+        ></el-switch>
+        <el-button class="ml-2" type="primary" @click="toAdd">新增</el-button>
+      </div>
+    </div>
+
+    <div class="part-box part-box-pad">
+      <el-table :data="groupList" border>
+        <el-table-column type="index" width="50"> </el-table-column>
+        <el-table-column label="评卷员">
+          <template slot-scope="scope">
+            <el-tag
+              v-for="user in scope.row.markerList"
+              :key="user.id"
+              class="tag-spin"
+              size="medium"
+            >
+              {{ user.name }}({{ user.orgName }})
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="评卷方式" width="100">
+          <template slot-scope="scope">
+            {{ scope.row.doubleEnable ? "双评" : "单评" }}
+          </template>
+        </el-table-column>
+        <el-table-column label="仲裁阀值" width="100">
+          <template v-if="scope.row.doubleEnable" slot-scope="scope">
+            {{ scope.row.arbitrateThreshold }}
+          </template>
+        </el-table-column>
+        <el-table-column label="合分策略" width="100">
+          <template v-if="scope.row.doubleEnable" slot-scope="scope">
+            {{ SCORE_POLICY_TYPE[scope.row.scorePolicy] }}
+          </template>
+        </el-table-column>
+        <el-table-column label="双评比例" width="100">
+          <template v-if="scope.row.doubleEnable" slot-scope="scope">
+            {{ scope.row.doubleRate }}%
+          </template>
+        </el-table-column>
+        <el-table-column label="评阅题目">
+          <template slot-scope="scope">
+            {{ scope.row.questions | questionsFilter }}
+          </template>
+        </el-table-column>
+        <el-table-column label="评卷区" width="80" align="center">
+          <template slot-scope="scope">
+            <i
+              v-if="scope.row.pictureConfigs.length"
+              class="el-icon-success color-success"
+            ></i>
+          </template>
+        </el-table-column>
+        <el-table-column class-name="action-column" label="操作" width="160px">
+          <template slot-scope="scope">
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              class="btn-primary"
+              type="text"
+              :disabled="!paperList.length"
+              @click="toSetArea(scope.row)"
+              >评卷区</el-button
+            >
+            <el-button
+              class="btn-danger"
+              type="text"
+              @click="toDelete(scope.row)"
+              >删除</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+      <p v-if="unsetQuestionNos.length" class="tips-info tips-error">
+        未设置试题:{{ unsetQuestionNos.join(",") }}
+      </p>
+    </div>
+
+    <!-- ModifyMarkGroup -->
+    <modify-mark-group
+      ref="ModifyMarkGroup"
+      :course-code="basicInfo.courseCode"
+      :instance="curGroupInfo"
+      :disabled-question-nos="disabledQuestionNos"
+      :paper-structure="subjectiveQuestionList"
+      @modified="groupModified"
+    ></modify-mark-group>
+    <!-- ModifyMarkArea -->
+    <modify-mark-area
+      ref="ModifyMarkArea"
+      :base-info="basicInfo"
+      :group="curGroupInfo"
+      :paper-list="paperList"
+      @modified="areaModified"
+    ></modify-mark-area>
+  </div>
+</template>
+
+<script>
+import { mapState, mapMutations } from "vuex";
+import ModifyMarkGroup from "./ModifyMarkGroup.vue";
+import ModifyMarkArea from "./ModifyMarkArea.vue";
+import {
+  examStructureFindJpg,
+  markGroupItemSave,
+  markGroupItemUpdate,
+  markGroupItemDelete,
+  markGroupAreaSave,
+  markClassStatusUpdate,
+} from "../../api";
+import { cardDetail } from "../../../card/api";
+import { deepCopy, maxNum } from "@/plugins/utils";
+import { SCORE_POLICY_TYPE } from "@/constants/enumerate";
+import { omit, pick } from "lodash";
+
+export default {
+  name: "mark-param-group",
+  components: {
+    ModifyMarkGroup,
+    ModifyMarkArea,
+  },
+  data() {
+    return {
+      markClassIsOpen: false,
+      SCORE_POLICY_TYPE,
+      questionCount: 0,
+      groupQuestionCount: 0,
+      groupList: [],
+      disabledQuestionNos: [],
+      unsetQuestionNos: [],
+      curGroupInfo: {},
+      subjectiveQuestionList: [],
+      subjectiveQuestionCount: 0,
+      objectiveQuestionCount: 0,
+      MARK_TYPE: {
+        0: "单评",
+        1: "双评",
+      },
+      paperList: [],
+      cardPages: [],
+      dataReady: false,
+      deleting: false,
+    };
+  },
+  computed: {
+    ...mapState("markParam", [
+      "basicInfo",
+      "paperStructureInfo",
+      "openMarkClass",
+      "groupInfo",
+    ]),
+  },
+  filters: {
+    questionsFilter(val) {
+      return val
+        .map((item) => `${item.mainNumber}-${item.subNumber}`)
+        .join(",");
+    },
+  },
+  mounted() {
+    this.initData();
+    this.getPaperList();
+    this.getCardPages();
+  },
+  methods: {
+    ...mapMutations("markParam", ["setGroupInfo", "setOpenMarkClass"]),
+    async getPaperList() {
+      this.paperList = [];
+      const data = await examStructureFindJpg({
+        examId: this.basicInfo.examId,
+        courseCode: this.basicInfo.courseCode,
+        paperNumber: this.basicInfo.paperNumber,
+        paperType: this.basicInfo.paperType,
+      });
+      const papers = data || [];
+      papers.sort((a, b) => a.index - b.index);
+      this.paperList = papers.map((paper) => {
+        return {
+          imgUrl: paper.path,
+          areas: [],
+        };
+      });
+    },
+    async getCardPages() {
+      const detData = await cardDetail(this.basicInfo.cardId);
+      const cardContent = JSON.parse(detData.content);
+      this.cardPages = cardContent.pages;
+    },
+    initData() {
+      this.markClassIsOpen = this.openMarkClass;
+      this.groupList = this.groupInfo.map((item) => {
+        return { ...deepCopy(item), id: this.$randomCode() };
+      });
+      this.questionCount = this.paperStructureInfo.length;
+      this.updateDisableQuestionNos();
+
+      this.subjectiveQuestionList = this.paperStructureInfo
+        .filter((item) => !item.isObjective)
+        .map((item) => {
+          return {
+            ...item,
+            qno: `${item.mainNumber}-${item.subNumber}`,
+          };
+        });
+      this.subjectiveQuestionCount = this.subjectiveQuestionList.length;
+      this.objectiveQuestionCount =
+        this.questionCount - this.subjectiveQuestionCount;
+
+      this.dataReady = true;
+    },
+    updateDisableQuestionNos(filterId) {
+      let groupList = this.groupList;
+      if (filterId)
+        groupList = groupList.filter((item) => item.id !== filterId);
+      let disabledQuestionNos = [];
+      groupList.forEach((item) => {
+        disabledQuestionNos = [
+          ...disabledQuestionNos,
+          ...item.questions.map((item) => item.qno),
+        ];
+      });
+      this.disabledQuestionNos = disabledQuestionNos;
+      if (!filterId) this.groupQuestionCount = disabledQuestionNos.length;
+      this.updateUnsetQuestionNos();
+    },
+    updateUnsetQuestionNos() {
+      this.unsetQuestionNos = this.subjectiveQuestionList
+        .filter((q) => !this.disabledQuestionNos.includes(q.qno))
+        .map((q) => q.qno);
+    },
+    toAdd() {
+      this.updateDisableQuestionNos();
+      if (this.groupQuestionCount === this.subjectiveQuestionCount) {
+        this.$message.error("当前已经没有主观题目可供设置!");
+        return;
+      }
+
+      this.curGroupInfo = {
+        id: this.$randomCode(),
+        groupNumber: maxNum(this.groupList.map((item) => item.groupNumber)) + 1,
+        doubleEnable: false,
+        doubleRate: 100,
+        arbitrateThreshold: 1,
+        scorePolicy: "AVG",
+        markers: [],
+        pictureConfigs: [],
+        questions: [],
+      };
+      this.$refs.ModifyMarkGroup.open();
+    },
+    toEdit(row) {
+      this.curGroupInfo = row;
+      this.updateDisableQuestionNos(row.id);
+      this.$refs.ModifyMarkGroup.open();
+    },
+    toSetArea(row) {
+      this.curGroupInfo = row;
+      this.$refs.ModifyMarkArea.open();
+    },
+    async toDelete(row) {
+      if (!this.deleting) return;
+
+      const confirm = await this.$confirm(`确定要删除当前分组吗?`, "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      this.deleting = true;
+      const res = await markGroupItemDelete({
+        examId: this.basicInfo.examId,
+        paperNumber: this.basicInfo.paperNumber,
+        groupNumber: row.groupNumber,
+      }).catch(() => {});
+      this.deleting = false;
+      if (!res) return;
+
+      const pos = this.groupList.findIndex((item) => item.id === row.id);
+      this.groupList.splice(pos, 1);
+      this.updateDisableQuestionNos();
+      this.updateData();
+    },
+    async groupModified(row) {
+      const pos = this.groupList.findIndex((item) => item.id === row.id);
+      if (!row.pictureConfigs.length) {
+        row.pictureConfigs = this.autoParsePictureConfigList(row.questions);
+      }
+
+      await this.updateMarkGroup(row, pos === -1);
+      if (pos === -1) {
+        this.groupList.push(row);
+      } else {
+        this.groupList.splice(pos, 1, row);
+      }
+      this.updateDisableQuestionNos();
+      this.updateData();
+    },
+    async updateMarkGroup(row, isAdd) {
+      const data = {
+        examId: this.basicInfo.examId,
+        paperNumber: this.basicInfo.paperNumber,
+        groupInfo: omit(row, ["id"]),
+      };
+      const updateApi = isAdd ? markGroupItemSave : markGroupItemUpdate;
+      await updateApi(data);
+    },
+    autoParsePictureConfigList(questions) {
+      if (!questions.length) return [];
+      let pictureConfigs = [];
+      const structs = questions.map(
+        (item) => `${item.mainNumber}_${item.subNumber}`
+      );
+      this.cardPages.forEach((page, pindex) => {
+        page.exchange.answer_area.forEach((area) => {
+          const [x, y, w, h] = area.area;
+          const qStruct = `${area.main_number}_${area.sub_number}`;
+
+          const pConfig = {
+            i: pindex + 1,
+            x,
+            y,
+            w,
+            h,
+            qStruct,
+          };
+
+          if (typeof area.sub_number === "number") {
+            if (!structs.includes(qStruct)) return;
+            pictureConfigs.push(pConfig);
+            return;
+          }
+          // 复合区域处理,比如填空题,多个小题合并为一个区域
+          if (typeof area.sub_number === "string") {
+            const areaStructs = area.sub_number
+              .split(",")
+              .map((subNumber) => `${area.main_number}_${subNumber}`);
+            if (
+              structs.some((struct) => areaStructs.includes(struct)) &&
+              !pictureConfigs.find((item) => item.qStruct === qStruct)
+            ) {
+              pictureConfigs.push(pConfig);
+            }
+          }
+        });
+      });
+      pictureConfigs.forEach((item) => {
+        delete item.qStruct;
+      });
+
+      // 合并相邻区域
+      pictureConfigs.sort((a, b) => {
+        return a.i - b.i || a.x - b.x || a.y - b.y;
+      });
+      let combinePictureConfigList = [];
+      let prevConfig = null;
+      pictureConfigs.forEach((item, index) => {
+        if (!index) {
+          prevConfig = { ...item };
+          combinePictureConfigList.push(prevConfig);
+          return;
+        }
+
+        if (
+          prevConfig.i === item.i &&
+          prevConfig.y + prevConfig.h >= item.y &&
+          prevConfig.w === item.w &&
+          prevConfig.x === item.x
+        ) {
+          prevConfig.h = item.y + item.h - prevConfig.y;
+        } else {
+          prevConfig = { ...item };
+          combinePictureConfigList.push(prevConfig);
+        }
+      });
+      // console.log(combinePictureConfigList);
+      // 自动扩展区域。
+      let scaleRate = 0.002;
+      combinePictureConfigList = combinePictureConfigList.map((item) => {
+        return {
+          i: item.i,
+          x: item.x - scaleRate,
+          y: item.y - scaleRate,
+          w: item.w + 2 * scaleRate,
+          h: item.h + 2 * scaleRate,
+        };
+      });
+
+      return combinePictureConfigList;
+    },
+    async areaModified(row) {
+      const pos = this.groupList.findIndex((item) => item.id === row.id);
+      if (pos === -1) return;
+
+      await markGroupAreaSave({
+        examId: this.basicInfo.examId,
+        paperNumber: this.basicInfo.paperNumber,
+        groupNumber: row.groupNumber,
+        pictureConfigs: row.pictureConfigs,
+      });
+      this.groupList.splice(pos, 1, row);
+      this.updateData();
+    },
+    async markClassChange() {
+      const name = this.markClassIsOpen ? "开启" : "取消";
+      const confirm = await this.$confirm(`确定要${name}分班阅卷吗?`, "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (confirm !== "confirm") {
+        this.markClassIsOpen = !this.markClassIsOpen;
+        return;
+      }
+
+      const res = await markClassStatusUpdate({
+        examId: this.basicInfo.examId,
+        paperNumber: this.basicInfo.paperNumber,
+        openMarkClass: this.markClassIsOpen,
+      }).catch(() => {
+        this.markClassIsOpen = !this.markClassIsOpen;
+      });
+      if (!res) return;
+      this.setOpenMarkClass(this.markClassIsOpen);
+    },
+    checkData() {
+      let errorMessages = [];
+
+      if (this.subjectiveQuestionCount > this.groupQuestionCount) {
+        errorMessages.push("当前还有题目未设置分组");
+      }
+
+      this.groupList.forEach((item, index) => {
+        if (item.doubleRate === 1 && !item.arbitrateThreshold) {
+          errorMessages.push(`序号${index + 1}设置中,仲裁阀值不能为空`);
+        }
+      });
+
+      if (errorMessages.length) {
+        this.$message.error(errorMessages.join("。"));
+        return;
+      }
+    },
+    getData() {
+      return this.groupList.map((item) => {
+        const group = omit(item, ["id"]);
+        group.questions = group.questions.map((item) =>
+          pick(item, [
+            "id",
+            "mainNumber",
+            "mainTitle",
+            "subNumber",
+            "questionType",
+          ])
+        );
+      });
+    },
+    updateData() {
+      this.setGroupInfo(this.getData());
+    },
+  },
+};
+</script>

+ 465 - 0
src/modules/mark/components/markParam/MarkParamStructure.vue

@@ -0,0 +1,465 @@
+<template>
+  <div class="mark-param-structure">
+    <div class="part-box part-box-pad">
+      <p class="tips-info">
+        1.请确认展示的试卷结构与提交的试卷、答题卡是否一致?
+      </p>
+      <p class="tips-info">2.请补充所有题目的小题分值,并确认试卷总分!</p>
+      <p class="tips-info tips-error">
+        3.开始阅卷后不允许修改试卷结构,请确认清楚后再提交!
+      </p>
+    </div>
+
+    <div class="part-box part-box-pad mb-0">
+      <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>
+        <template v-if="structureCanEdit">
+          <el-table-column prop="mainTitle" label="大题名称">
+            <span slot-scope="scope" v-if="scope.row.mainFirstSub">
+              <el-input
+                v-model.trim="scope.row.mainTitle"
+                size="small"
+                :maxlength="32"
+                clearable
+                @change="mainTitleChange(scope.row)"
+              ></el-input>
+            </span>
+          </el-table-column>
+          <el-table-column prop="questionType" label="题型" width="120">
+            <template slot-scope="scope" v-if="scope.row.mainFirstSub">
+              <el-select
+                v-model="scope.row.questionType"
+                placeholder="请选择"
+                class="width-full"
+                @change="qTypeChange(scope.row)"
+              >
+                <el-option
+                  v-for="item in QUESTION_TYPE_LIST"
+                  :key="item.code"
+                  :value="item.code"
+                  :label="item.name"
+                >
+                </el-option>
+              </el-select>
+            </template>
+          </el-table-column>
+          <el-table-column label="选项个数" width="100">
+            <template
+              slot-scope="scope"
+              v-if="scope.row.questionType <= 2 && scope.row.mainFirstSub"
+            >
+              <el-input-number
+                v-model="scope.row.optionCount"
+                class="width-full"
+                size="small"
+                :min="2"
+                :max="26"
+                :step="1"
+                step-strictly
+                :controls="false"
+                @change="optionCountChange(scope.row)"
+              ></el-input-number>
+            </template>
+          </el-table-column>
+        </template>
+        <template v-else>
+          <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 label="题型" width="120">
+            <template slot-scope="scope" v-if="scope.row.mainFirstSub">
+              {{ questionTypeDict[scope.row.questionType] }}
+            </template>
+          </el-table-column>
+        </template>
+        <el-table-column label="每题分值" width="100">
+          <template slot-scope="scope" v-if="scope.row.mainFirstSub">
+            <el-input-number
+              v-model="scoresPerTopic[scope.row.mainId]"
+              class="width-full"
+              size="small"
+              :min="0.5"
+              :max="500"
+              :step="0.5"
+              step-strictly
+              :controls="false"
+              @change="(val) => scorePerTopicChange(val, scope.row)"
+            ></el-input-number>
+          </template>
+        </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">
+          <template slot-scope="scope">
+            <el-input-number
+              v-model="scope.row.totalScore"
+              class="width-80"
+              size="small"
+              :min="0.5"
+              :max="500"
+              :step="0.5"
+              step-strictly
+              :controls="false"
+            ></el-input-number>
+          </template>
+        </el-table-column>
+        <el-table-column
+          v-if="structureCanEdit"
+          class-name="action-column"
+          label="操作"
+          width="200px"
+        >
+          <template slot-scope="scope">
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toAddMain(scope.row)"
+              >新增大题</el-button
+            >
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toAddSub(scope.row)"
+              >新增小题</el-button
+            >
+            <el-button
+              :disabled="tableData.length <= 1"
+              class="btn-danger"
+              type="text"
+              @click="toDeleteSub(scope.row)"
+              >删除</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <div class="total-info">
+      试卷总分:<span>{{ paperTotalScore }}</span
+      >分
+    </div>
+
+    <div class="mark-footer">
+      <el-button type="primary" :disabled="loading" @click="submit"
+        >提交</el-button
+      >
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { calcSum } from "@/plugins/utils";
+import { QUESTION_TYPE_LIST } from "@/constants/enumerate";
+import { mapState, mapMutations } from "vuex";
+import { markStructureSave } from "../../api";
+import { omit } from "lodash";
+
+export default {
+  name: "mark-paper-structure",
+  data() {
+    return {
+      tableData: [],
+      QUESTION_TYPE_LIST,
+      questionTypeDict: {},
+      scoresPerTopic: {},
+      loading: false,
+    };
+  },
+  computed: {
+    ...mapState("markParam", [
+      "basicInfo",
+      "structureCanEdit",
+      "paperStructureInfo",
+    ]),
+    paperTotalScore() {
+      return calcSum(this.tableData.map((item) => item.totalScore || 0));
+    },
+  },
+  mounted() {
+    this.initData();
+  },
+  methods: {
+    ...mapMutations("markParam", ["setPaperStructureInfo"]),
+    initData() {
+      let questionTypeDict = {};
+      QUESTION_TYPE_LIST.forEach((item) => {
+        questionTypeDict[item.code] = item.name;
+      });
+      this.questionTypeDict = questionTypeDict;
+
+      let curMainNumber = null;
+      let scoresPerTopic = {};
+      this.tableData = this.paperStructureInfo.map((item) => {
+        let nitem = {
+          ...item,
+          key: this.$randomCode(),
+          mainFirstSub: false,
+          expandSub: true,
+        };
+        if (curMainNumber !== item.mainNumber) {
+          curMainNumber = item.mainNumber;
+          scoresPerTopic[curMainNumber] = undefined;
+          nitem.mainFirstSub = true;
+          nitem.mainId = this.$randomCode();
+        }
+        return nitem;
+      });
+      this.scoresPerTopic = scoresPerTopic;
+
+      if (!this.tableData.length && this.structureCanEdit) {
+        this.createMain();
+      }
+    },
+    getNewRow(val) {
+      return this.$objAssign(
+        {
+          id: null,
+          key: this.$randomCode(),
+          mainId: null,
+          isObjective: true,
+          mainNumber: 1,
+          subNumber: 1,
+          mainTitle: "",
+          answer: "",
+          totalScore: undefined,
+          intervalScore: undefined,
+          objectivePolicy: null,
+          questionType: null,
+          optionCount: undefined,
+          mainFirstSub: true,
+          expandSub: true,
+        },
+        val
+      );
+    },
+    createMain() {
+      this.tableData.push(this.getNewRow({}));
+    },
+    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(" ");
+    },
+    getNextMainStartPos(startPos, curMainId) {
+      let nextMainStartPos = null;
+      for (let i = startPos, len = this.tableData.length; i < len; i++) {
+        const element = this.tableData[i];
+        if (element.mainId !== curMainId) {
+          nextMainStartPos = i;
+          return nextMainStartPos;
+        }
+      }
+      if (nextMainStartPos === null) return this.tableData.length;
+    },
+    toAddMain(row) {
+      const startPos = this.tableData.findIndex((item) => item.id === row.id);
+      let nextMainStartPos = this.getNextMainStartPos(startPos, row.mainId);
+      this.tableData.splice(
+        nextMainStartPos,
+        0,
+        this.getNewRow({
+          mainId: this.$randomCode(),
+          mainNumber: row.mainNumber + 1,
+          totalScore: row.totalScore,
+          questionType: row.questionType,
+        })
+      );
+      this.updateMainData();
+    },
+    updateMainData() {
+      let curMainNumber = 0,
+        curMainId = null;
+      this.tableData.forEach((item) => {
+        if (item.mainId !== curMainId) {
+          curMainId = item.mainId;
+          curMainNumber++;
+        }
+        item.mainNumber = curMainNumber;
+      });
+    },
+    toAddSub(row) {
+      const subPos = this.tableData.findIndex((item) => item.id === row.id);
+      this.tableData.splice(
+        subPos + 1,
+        0,
+        this.getNewRow({
+          ...row,
+          mainFirstSub: false,
+          answer: "",
+          key: this.$randomCode(),
+        })
+      );
+      this.updateSubData(row.mainId);
+    },
+    updateSubData(mainId) {
+      this.tableData
+        .filter((item) => item.mainId === mainId)
+        .forEach((item, index) => {
+          item.subNumber = index + 1;
+        });
+    },
+    toDeleteSub(row) {
+      const subPos = this.tableData.findIndex((item) => item.id === row.id);
+      this.tableData.splice(subPos, 1);
+
+      this.tableData
+        .filter((item) => item.mainId === row.mainId)
+        .forEach((item, index) => {
+          item.mainFirstSub = !index;
+        });
+      this.updateSubData(row.mainId);
+      this.updateMainData();
+    },
+    switchExpandSub(row) {
+      row.expandSub = !row.expandSub;
+      this.tableData
+        .filter((item) => item.mainId === row.mainId && !item.mainFirstSub)
+        .forEach((item) => (item.expandSub = row.expandSub));
+    },
+    mainTitleChange(row) {
+      this.tableData
+        .filter((item) => item.mainId === row.mainId && !item.mainFirstSub)
+        .forEach((item) => (item.mainTitle = row.mainTitle));
+    },
+    qTypeChange(row) {
+      const curQt = this.QUESTION_TYPE_LIST.find(
+        (item) => item.code === row.type
+      );
+      if (!curQt) return;
+
+      this.tableData
+        .filter((item) => item.mainId === row.mainId)
+        .forEach((item) => {
+          item.questionType = curQt.code;
+          item.isObjective = curQt.qType === "objective";
+          item.optionCount = curQt.optionCount;
+        });
+    },
+    optionCountChange(row) {
+      if (!row.optionCount) return;
+
+      this.tableData
+        .filter((item) => item.mainId === row.mainId && !item.mainFirstSub)
+        .forEach((item) => {
+          item.optionCount = row.optionCount;
+        });
+    },
+    scorePerTopicChange(val, row) {
+      if (!val) return;
+      this.tableData
+        .filter((item) => item.mainId === row.mainId)
+        .forEach((item) => {
+          item.totalScore = val;
+        });
+    },
+    checkData() {
+      let errorMessages = [];
+      this.tableData.forEach((item) => {
+        let errorMsg = ``;
+        if (item.mainFirstSub) {
+          let errorFields = [];
+          if (!item.mainTitle) {
+            errorFields.push("大题名称");
+          }
+          if (!item.mainNumber) {
+            errorFields.push("大题号");
+          }
+          if (!item.optionCount && item.questionType <= 2) {
+            errorFields.push("选项个数");
+          }
+          if (errorFields.length) {
+            errorMsg += `${errorFields.join("、")}不能为空,`;
+          }
+        }
+
+        let errorFields = [];
+        if (!item.subNumber) {
+          errorFields.push("小题号");
+        }
+        if (!item.totalScore) {
+          errorFields.push("小题满分");
+        }
+        if (!item.intervalScore) {
+          errorFields.push("评卷间隔分");
+        }
+        if (errorFields.length) {
+          errorMsg += `第${item.subNumber}小题,${errorFields.join(
+            "、"
+          )}不能为空,`;
+        }
+
+        if (errorMsg) {
+          errorMsg = `第${item.mainNumber}大题,${errorMsg}`;
+          errorMessages.push(errorMsg);
+        }
+      });
+
+      if (errorMessages.length) {
+        this.$message.error(errorMessages.join("。"));
+        return;
+      }
+
+      return true;
+    },
+    getData() {
+      return this.tableData.map((item) => {
+        return omit(item, ["key", "mainId", "expandSub"]);
+      });
+    },
+    async submit() {
+      if (this.loading) return;
+      if (!this.checkData()) return;
+
+      this.loading = true;
+      const questions = this.getData();
+      const res = await markStructureSave({
+        examId: this.basicInfo.examId,
+        paperNumber: this.basicInfo.paperNumber,
+        questions,
+      }).catch(() => {});
+      this.loading = false;
+      if (!res) return;
+      this.$message.success("保存成功!");
+      this.setPaperStructureInfo(questions);
+      this.$emit("confirm");
+    },
+    cancel() {
+      this.$emit("cancel");
+    },
+  },
+};
+</script>

+ 155 - 0
src/modules/mark/components/markParam/ModifyMarkArea.vue

@@ -0,0 +1,155 @@
+<template>
+  <el-dialog
+    class="modify-mark-area"
+    :visible.sync="modalIsShow"
+    top="0"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    fullscreen
+    :show-close="false"
+    @open="visibleChange"
+  >
+    <div class="box-justify" slot="title">
+      <h2 class="el-dialog__title">设置分组评卷区域</h2>
+      <div>
+        <el-button type="success" @click="confirm">确定</el-button>
+        <el-button @click="cancel">取消</el-button>
+      </div>
+    </div>
+    <div class="tips-info">
+      <i class="el-icon-warning"></i>
+      按住鼠标左键拖动,框选评卷区域。可以设置多个评卷区域。当不设置评卷区域时,评卷时将展示答题卡所有答题区。
+    </div>
+    <div v-if="modalIsShow" class="area-container">
+      <area-cropper
+        v-for="(paper, index) in papers"
+        :id="`area-cropper-${index}`"
+        :imgUrl="paper.imgUrl"
+        ref="AreaCropper"
+        :key="paper.imgUrl"
+        :paper="paper"
+        @curarea-change="cropperCurareaChange"
+        @change="(areas) => areaChange(index, areas)"
+        @paper-load="() => areaPaperLoaded(index)"
+      ></area-cropper>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import AreaCropper from "./areaCropper/AreaCropper.vue";
+
+export default {
+  name: "modify-mark-area",
+  components: { AreaCropper },
+  props: {
+    baseInfo: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    group: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    paperList: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      papers: [],
+      paperLoadedList: [],
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.paperLoadedList = [];
+      this.papers = this.paperList.map((paper) => {
+        let npaper = { ...paper };
+        npaper.areas = [];
+        return npaper;
+      });
+      this.group.pictureConfigs.forEach((config) => {
+        const index = config.i - 1;
+        this.papers[index].areas.push({ ...config });
+      });
+    },
+    areaPaperLoaded(paperIndex) {
+      if (this.paperLoadedList.includes(paperIndex)) return;
+      this.paperLoadedList.push(paperIndex);
+      if (this.paperLoadedList.length === this.paperList.length) {
+        this.scrollToFirstArea();
+      }
+    },
+    scrollToFirstArea() {
+      if (!this.group.pictureConfigs.length) return;
+      let paperIndexs = this.group.pictureConfigs.map((item) => item.i - 1);
+      paperIndexs.sort((a, b) => a - b);
+      const firstPaperIndex = paperIndexs[0];
+
+      this.$nextTick(() => {
+        const areaCropperDom = document.getElementById(
+          `area-cropper-${firstPaperIndex}`
+        );
+        let areaItems = areaCropperDom.querySelectorAll(".element-resize");
+        let firstAreaItem = null;
+        let topNum = 9999;
+        for (let i = 0; i < areaItems.length; i++) {
+          const element = areaItems[i];
+          if (element.offsetTop < topNum) {
+            topNum = element.offsetTop;
+            firstAreaItem = element;
+          }
+        }
+        // console.log(firstAreaItem);
+        const scrollTop =
+          areaCropperDom.offsetTop + firstAreaItem.offsetTop - 100;
+        this.$el.querySelector(".el-dialog").scrollTop = scrollTop;
+      });
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    areaChange(index, areas) {
+      this.papers[index].areas = areas;
+    },
+    cropperCurareaChange(area) {
+      this.$refs.AreaCropper.forEach((cropper) => {
+        cropper.curareaChange(area);
+      });
+    },
+    confirm() {
+      let areas = [];
+      this.papers.forEach((paper, pindex) => {
+        if (!paper.areas.length) return;
+        paper.areas.forEach((area) => {
+          let narea = { i: pindex + 1 };
+          if (!area.isFull) {
+            narea = { ...narea, x: area.x, y: area.y, w: area.w, h: area.h };
+          }
+          areas.push(narea);
+        });
+      });
+      this.$emit(
+        "modified",
+        Object.assign({}, this.group, { pictureConfigs: areas })
+      );
+      this.cancel();
+    },
+  },
+};
+</script>

+ 616 - 0
src/modules/mark/components/markParam/ModifyMarkGroup.vue

@@ -0,0 +1,616 @@
+<template>
+  <el-dialog
+    class="modify-mark-group"
+    :visible.sync="modalIsShow"
+    append-to-body
+    top="0"
+    width="900px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :show-close="false"
+    @opened="visibleChange"
+  >
+    <div slot="title"></div>
+    <el-form ref="rowInfoFormRef" :model="rowInfo" :rules="rowInfoRules" line>
+      <el-form-item label="评卷方式">
+        <el-radio-group v-model="rowInfo.doubleEnable">
+          <el-radio :label="false">单评</el-radio>
+          <el-radio :label="true">双评</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <br />
+      <template v-if="rowInfo.doubleEnable">
+        <el-form-item label="仲裁阀值" prop="arbitrateThreshold">
+          <el-input-number
+            v-model="rowInfo.arbitrateThreshold"
+            class="width-80"
+            size="small"
+            :min="0"
+            :max="999999"
+            :step="0.01"
+            step-strictly
+            :controls="false"
+          >
+          </el-input-number>
+        </el-form-item>
+        <el-form-item label="合分策略" prop="scorePolicy">
+          <el-select v-model="rowInfo.scorePolicy" class="width-80">
+            <el-option
+              v-for="(val, key) in SCORE_POLICY_TYPE"
+              :key="key"
+              :label="val"
+              :value="key"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="双评比例" prop="doubleRate">
+          <el-input-number
+            v-model="rowInfo.doubleRate"
+            class="width-80"
+            size="small"
+            :min="1"
+            :max="100"
+            :step="1"
+            step-strictly
+            :controls="false"
+          >
+          </el-input-number
+          >%
+        </el-form-item>
+      </template>
+    </el-form>
+
+    <el-row type="flex" :gutter="10">
+      <el-col :span="12">
+        <div class="marker-box">
+          <div class="user-title">评卷员</div>
+          <div class="user-search">
+            <el-input
+              v-model="filterLabel"
+              placeholder="请输入评卷员名称"
+              clearable
+              size="mini"
+              prefix-icon="el-icon-search"
+              @input="labelChange"
+            ></el-input>
+          </div>
+          <div class="user-types">
+            <div
+              :class="['user-type', { 'is-active': userType === 'org' }]"
+              @click="switchUserType('org')"
+            >
+              组织架构
+            </div>
+            <div
+              :class="['user-type', { 'is-active': userType === 'course' }]"
+              @click="switchUserType('course')"
+            >
+              课程
+            </div>
+          </div>
+          <div class="user-tree">
+            <el-tree
+              ref="UserTree"
+              :data="userTree"
+              node-key="id"
+              :default-checked-keys="selectedUserIds"
+              :props="defaultProps"
+              default-expand-all
+            >
+              <span class="custom-tree-node" slot-scope="{ node, data }">
+                <el-checkbox
+                  v-if="data.isUser"
+                  v-model="node.checked"
+                  @change="(val) => userChange(val, data)"
+                >
+                  {{ node.label }}
+                </el-checkbox>
+                <span v-else>{{ node.label }}</span>
+                <div title="全选" @click.stop>
+                  <el-checkbox
+                    v-if="!data.isUser && data.children.length"
+                    v-model="data.selected"
+                    @change="(checked) => selectNodeAll(checked, data)"
+                  ></el-checkbox>
+                </div>
+              </span>
+            </el-tree>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="12">
+        <div class="marker-box marker-box-uq">
+          <el-form
+            ref="modalFormRef"
+            :rules="rules"
+            :model="{}"
+            label-width="100px"
+            label-position="top"
+          >
+            <el-form-item prop="users" label="已选评卷员:">
+              <el-tag
+                v-for="user in selectedUsers"
+                :key="user.id"
+                closable
+                :disable-transitions="false"
+                @close="toDeleteUser(user)"
+              >
+                {{ user.name }}({{ user.orgName }})
+              </el-tag>
+            </el-form-item>
+            <el-form-item
+              prop="questions"
+              :label="canSelectQuestion ? '选择评卷题目:' : '评卷题目:'"
+            >
+              <div class="marker-paper-struct">
+                <div
+                  v-for="mainItem in paperStructs"
+                  :key="mainItem.mainNumber"
+                  class="struct-item"
+                >
+                  <div class="struct-header box-justify">
+                    <h4>{{ mainItem.mainNumber }}、{{ mainItem.mainTitle }}</h4>
+                    <el-checkbox
+                      v-if="
+                        canSelectQuestion &&
+                        mainItem.children &&
+                        mainItem.children.length
+                      "
+                      v-model="mainItem.selected"
+                      title="全选"
+                      @change="
+                        (checked) => selectQuestionAll(checked, mainItem)
+                      "
+                    ></el-checkbox>
+                  </div>
+                  <div
+                    v-if="mainItem.children && mainItem.children.length"
+                    class="struct-questions"
+                  >
+                    <template v-if="canSelectQuestion">
+                      <el-checkbox
+                        v-for="question in mainItem.children"
+                        :key="question.id"
+                        v-model="question.selected"
+                        :disabled="question.disabled"
+                        @change="questionChange"
+                      >
+                        {{ question.subNumber }}
+                      </el-checkbox>
+                    </template>
+                    <template v-else>
+                      <template v-for="question in mainItem.children">
+                        <el-tag v-if="question.selected" :key="question.id">{{
+                          question.subNumber
+                        }}</el-tag>
+                      </template>
+                    </template>
+                  </div>
+                </div>
+              </div>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-col>
+    </el-row>
+
+    <div class="marker-footer">
+      <el-button type="primary" @click="confirm">确认</el-button>
+      <el-button @click="cancel">取消</el-button>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import { deepCopy } from "../../../../plugins/utils";
+import { organizationList } from "../../../base/api";
+import { SCORE_POLICY_TYPE } from "@/constants/enumerate";
+
+export default {
+  name: "modify-mark-group",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    disabledQuestionNos: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    paperStructure: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    courseCode: {
+      type: String,
+      default: "",
+    },
+    canSelectQuestion: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  data() {
+    const usersValidator = (rule, value, callback) => {
+      if (!this.selectedUserIds.length) {
+        callback(new Error("请选择评卷员"));
+      } else {
+        callback();
+      }
+    };
+    const questionsValidator = (rule, value, callback) => {
+      if (!this.selectedQuestionNos.length) {
+        callback(new Error("请选择试题"));
+      } else {
+        callback();
+      }
+    };
+
+    return {
+      modalIsShow: false,
+      SCORE_POLICY_TYPE,
+      filterLabel: "",
+      userType: "course",
+      courseUsers: [],
+      orgUsers: [],
+      userTree: [],
+      userList: [],
+      selectedUsers: [],
+      selectedUserIds: [],
+      selectedQuestions: [],
+      selectedQuestionNos: [],
+      paperStructs: [],
+      defaultProps: {
+        children: "children",
+        label: "label",
+      },
+      rules: {
+        users: [
+          {
+            required: true,
+            validator: usersValidator,
+            trigger: "change",
+          },
+        ],
+        questions: [
+          {
+            required: true,
+            validator: questionsValidator,
+            trigger: "change",
+          },
+        ],
+      },
+      rowInfo: {},
+      rowInfoRules: {
+        doubleRate: [
+          {
+            required: true,
+            message: "请输入双评比例",
+            trigger: "change",
+          },
+        ],
+        arbitrateThreshold: [
+          {
+            required: true,
+            message: "请输入仲裁阀值",
+            trigger: "change",
+          },
+        ],
+        scorePolicy: [
+          {
+            required: true,
+            message: "请选择合分策略",
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  mounted() {
+    this.getOrgData();
+  },
+  methods: {
+    visibleChange() {
+      this.parseStructs();
+      this.filterLabel = "";
+      this.userType = "course";
+      this.selectedQuestions = this.instance.questions;
+      this.selectedQuestionNos = this.selectedQuestions.map((item) => item.id);
+      this.selectedUsers = this.instance.markers.map((item) => {
+        return { ...item };
+      });
+      this.selectedUserIds = this.instance.markers.map((item) => item.id);
+      this.labelChange();
+      this.rowInfo = this.$objAssign(
+        {
+          doubleEnable: false,
+          doubleRate: 100,
+          arbitrateThreshold: 1,
+          scorePolicy: "AVG",
+        },
+        this.instance
+      );
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    // user
+    switchUserType(type) {
+      this.filterLabel = "";
+      this.userType = type;
+
+      this.userTree =
+        type === "org" ? deepCopy(this.orgUsers) : deepCopy(this.courseUsers);
+      this.$refs.UserTree.setCheckedKeys(this.selectedUserIds);
+    },
+    async getOrgData() {
+      let params = {
+        specialPrivilege: "MARKER",
+      };
+      if (this.courseCode) params.courseCode = this.courseCode;
+      const data = await organizationList(params);
+      this.parseUserData(data);
+      this.getUserList();
+    },
+    parseUserData(data) {
+      const parseUser = (list) => {
+        return list.map((item) => {
+          // org
+          let nitem = {
+            id: item.id,
+            label: item.name,
+            isUser: false,
+            selected: false,
+            children: [],
+          };
+
+          if (item["children"] && item["children"].length) {
+            nitem.children = [...nitem.children, ...parseUser(item.children)];
+          }
+          // user
+          if (item["sysUserList"] && item["sysUserList"].length) {
+            let sysUserList = item.sysUserList;
+            const users = sysUserList.map((user) => {
+              const nuser = {
+                id: user.id,
+                userId: user.id,
+                label: user.realName,
+                name: user.realName,
+                orgName: item.name,
+                loginName: user.loginName,
+                selected: false,
+                isUser: true,
+              };
+              return nuser;
+            });
+            nitem.children = [...nitem.children, ...users];
+          }
+
+          if (item["courseUserList"] && item["courseUserList"].length) {
+            nitem.courseUserList = item.courseUserList.map((user) => {
+              return {
+                id: user.id,
+                userId: user.id,
+                label: user.realName,
+                name: user.realName,
+                orgName: user.orgName,
+                loginName: user.loginName,
+                selected: false,
+                isUser: true,
+              };
+            });
+          }
+          return nitem;
+        });
+      };
+      this.orgUsers = parseUser(data);
+      this.userTree = deepCopy(this.orgUsers);
+      if (this.courseCode && this.orgUsers[0].courseUserList) {
+        this.courseUsers = deepCopy(this.orgUsers[0].courseUserList);
+      }
+      this.getUserList();
+    },
+    getUserList() {
+      let userList = [];
+      const fetchUser = (users) => {
+        users.forEach((item) => {
+          if (item["children"] && item["children"].length) {
+            fetchUser(item.children);
+          } else {
+            if (item.isUser) {
+              let nitem = { ...item };
+              nitem.label = `${nitem.name}(${nitem.orgName})`;
+              userList.push(nitem);
+            }
+          }
+        });
+      };
+      fetchUser(this.orgUsers);
+
+      this.userList = userList;
+    },
+    labelChange() {
+      if (!this.filterLabel) {
+        this.switchUserType(this.userType);
+      } else {
+        const escapeRegexpString = (value = "") =>
+          String(value).replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
+        const reg = new RegExp(escapeRegexpString(this.filterLabel), "i");
+
+        if (this.userType === "org") {
+          this.userTree = this.userList.filter((item) => reg.test(item.name));
+        } else {
+          this.userTree = this.courseUsers.filter((item) =>
+            reg.test(item.name)
+          );
+        }
+      }
+      this.$refs.UserTree.setCheckedKeys(this.selectedUserIds);
+    },
+    selectNodeAll(checked, data) {
+      let users = [];
+      const getUserIds = (list) => {
+        list.forEach((item) => {
+          item.selected = checked;
+          if (item.children && item.children.length) {
+            getUserIds(item.children);
+          } else {
+            if (item.isUser) users.push(item);
+          }
+        });
+      };
+      getUserIds(data.children);
+
+      const userIds = users.map((u) => u.id);
+      const selectedUserIds = this.selectedUsers.map((item) => item.id);
+
+      let deleteUserIds = [];
+      userIds.forEach((userId, uindex) => {
+        const userPos = selectedUserIds.indexOf(userId);
+        const includeUser = userPos !== -1;
+        if (checked) {
+          if (!includeUser) this.selectedUsers.push(users[uindex]);
+        } else {
+          if (includeUser) {
+            deleteUserIds.push(userId);
+          }
+        }
+      });
+      this.selectedUsers = this.selectedUsers.filter(
+        (u) => !deleteUserIds.includes(u.id)
+      );
+      this.selectedUserIds = this.selectedUsers.map((item) => item.id);
+      this.$refs.UserTree.setCheckedKeys(this.selectedUserIds);
+      this.$refs.modalFormRef.validateField("users");
+    },
+    updateSelectedUsersFromUserIds() {
+      this.selectedUsers = this.userList.filter((user) =>
+        this.selectedUserIds.includes(user.id)
+      );
+    },
+    userChange(checked, user) {
+      if (checked) {
+        this.selectedUsers.push(user);
+      } else {
+        this.selectedUsers = this.selectedUsers.filter(
+          (item) => item.id !== user.id
+        );
+      }
+      this.selectedUserIds = this.selectedUsers.map((item) => item.id);
+      this.$refs.modalFormRef.validateField("users");
+    },
+    toDeleteUser(user) {
+      const pos = this.selectedUsers.findIndex((item) => item.id === user.id);
+      this.selectedUsers.splice(pos, 1);
+      this.selectedUserIds = this.selectedUsers.map((item) => item.id);
+      this.$refs.UserTree.setCheckedKeys(this.selectedUserIds);
+      this.$refs.modalFormRef.validateField("users");
+    },
+    // question
+    parseStructs() {
+      this.selectedQuestionNos = this.instance.questions.map((q) => q.qno);
+      let paperStructs = [];
+      let struct = null;
+      this.paperStructure.forEach((item) => {
+        if (item.mainFirstSub) {
+          if (struct) paperStructs.push(struct);
+          struct = {
+            mainTitle: item.mainTitle,
+            mainNumber: item.mainNumber,
+            selected: false,
+            children: [],
+          };
+        }
+        struct.children.push({
+          ...item,
+          disabled: this.disabledQuestionNos.includes(item.qno),
+          selected: this.selectedQuestionNos.includes(item.qno),
+        });
+      });
+      if (struct) paperStructs.push(struct);
+      this.paperStructs = paperStructs;
+    },
+    selectQuestionAll(checked, mainItem) {
+      mainItem.children.forEach((q) => {
+        if (!q.disabled) q.selected = checked;
+      });
+      this.questionChange();
+    },
+    updateSelectedQuestions() {
+      let selectedQuestions = [];
+      let selectedQuestionNos = [];
+      this.paperStructs.forEach((s) => {
+        s.children.forEach((q) => {
+          if (q.selected && !q.disabled) {
+            selectedQuestions.push(q);
+            selectedQuestionNos.push(q.qno);
+          }
+        });
+      });
+      this.selectedQuestions = selectedQuestions;
+      this.selectedQuestionNos = selectedQuestionNos;
+    },
+    questionChange() {
+      this.updateSelectedQuestions();
+      this.$refs.modalFormRef.validateField("questions");
+    },
+    getQuestionStruct(questions) {
+      let structs = questions.map((item) => {
+        return {
+          mainNumber: item.mainNumber,
+          subNumber: item.subNumber,
+        };
+      });
+      structs.sort(
+        (a, b) => a.mainNumber - b.mainNumber || a.subNumber - b.subNumber
+      );
+      return structs
+        .map((item) => `${item.mainNumber}_${item.subNumber}`)
+        .join();
+    },
+    // confirm
+    async confirm() {
+      const valid1 = await this.$refs.rowInfoFormRef.validate().catch(() => {});
+      const valid2 = await this.$refs.modalFormRef.validate().catch(() => {});
+      if (!valid1 || !valid2) return;
+
+      let datas = { ...this.instance };
+      datas.markers = this.selectedUsers.map((item) => {
+        return {
+          id: item.id,
+          userId: item.id,
+          name: item.name,
+          loginName: item.loginName,
+          orgName: item.orgName,
+        };
+      });
+      datas.questions = this.selectedQuestions.map((item) => {
+        let nitem = { ...item };
+        delete nitem.disabled;
+        delete nitem.selected;
+        return nitem;
+      });
+      // 评卷题目变动时,清空评卷区
+      if (
+        this.getQuestionStruct(this.instance.questions) !==
+        this.getQuestionStruct(datas.questions)
+      ) {
+        datas.pictureConfigs = [];
+      }
+      this.$emit("modified", datas);
+      this.cancel();
+    },
+  },
+};
+</script>

+ 184 - 0
src/modules/mark/components/markParam/ModifyMarkParams.vue

@@ -0,0 +1,184 @@
+<template>
+  <el-dialog
+    class="modify-mark-params"
+    :visible.sync="modalIsShow"
+    top="0"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :show-close="false"
+    append-to-body
+    fullscreen
+    @open="initData"
+  >
+    <div slot="title">
+      <h2 class="el-dialog__title">评卷参数设置</h2>
+      <span
+        >课程名称:{{ instance.courseName }}({{ instance.courseCode }})</span
+      >
+      <button class="el-dialog__headerbtn" @click="cancel"></button>
+    </div>
+
+    <div class="mb-4 tab-btns">
+      <el-button
+        v-for="tab in tabs"
+        :key="tab.val"
+        size="medium"
+        :type="curTab == tab.val ? 'primary' : 'default'"
+        :disabled="checkTabValid(tab)"
+        @click="selectMenu(tab.val)"
+        >{{ tab.name }}
+      </el-button>
+    </div>
+
+    <div v-if="dataReady">
+      <component
+        :is="currentComponent"
+        def="MarkParamRef"
+        @cancel="cancel"
+        @confirm="confirm"
+      ></component>
+    </div>
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<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 { markStructureList, markGroupList } from "../../api";
+
+export default {
+  name: "modify-mark-params",
+  components: {
+    MarkParamStructure,
+    // MarkParamGroup,
+    // MarkParamClass,
+    // MarkParamObjectiveAnswer,
+    // MarkParamSubjectiveAnswer,
+  },
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      dataReady: false,
+      // step
+      curTab: "structure",
+      tabs: [
+        {
+          name: "试卷结构设置",
+          val: "structure",
+        },
+        {
+          name: "评卷任务设置",
+          val: "group",
+        },
+        {
+          name: "分班阅卷设置",
+          val: "class",
+        },
+        {
+          name: "客观题标答设置",
+          val: "objective-answer",
+        },
+        {
+          name: "主观题标答设置",
+          val: "subjective-answer",
+        },
+      ],
+      current: 0,
+      qustionSubmit: false,
+    };
+  },
+  computed: {
+    ...mapState("markParam", ["openMarkClass"]),
+    currentComponent() {
+      return `mark-param-${this.curTab}`;
+    },
+    isFirstStep() {
+      return this.current === 0;
+    },
+    isLastStep() {
+      return this.current === this.lastStep;
+    },
+    lastStep() {
+      return this.tabs.length - 1;
+    },
+  },
+  methods: {
+    ...mapMutations("markParam", [
+      "setBasicInfo",
+      "setPaperStructureInfo",
+      "setStructureCanEdit",
+      "setGroupInfo",
+      "setOpenMarkClass",
+      "initStore",
+    ]),
+    async initData() {
+      this.setBasicInfo({ ...this.instance });
+      const params = {
+        examId: this.instance.examId,
+        paperNumber: this.instance.paperNumber,
+      };
+
+      // structure
+      const structRes = await markStructureList(params);
+      this.setPaperStructureInfo(structRes.questions || []);
+      this.setStructureCanEdit(structRes.canCreate);
+      this.qustionSubmit = structRes.qustionSubmit;
+      // group
+      const groupRes = await markGroupList(params);
+      this.setGroupInfo(groupRes.groups || []);
+      this.setOpenMarkClass(!!groupRes.openMarkClass);
+
+      this.selectMenu("structure");
+      this.dataReady = true;
+    },
+    checkTabValid(tab) {
+      return (
+        (!this.qustionSubmit && tab.val !== "structure") ||
+        (tab.val === "class" && !this.openMarkClass)
+      );
+    },
+    selectMenu(val) {
+      this.curTab = val;
+      this.current = this.tabs.findIndex((item) => item.val === val);
+    },
+    async cancel() {
+      const res = await this.$confirm("确定要退出阅卷参数编辑吗?", "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (res !== "confirm") return;
+
+      this.initStore();
+      this.dataReady = false;
+      this.$emit("modified");
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    confirm() {
+      if (this.isLastStep) {
+        this.selectMenu(this.tabs[0].val);
+      } else {
+        if (this.curTab === "group" && !this.openMarkClass) {
+          this.selectMenu(this.tabs[this.current + 2].val);
+        } else {
+          this.selectMenu(this.tabs[this.current + 1].val);
+        }
+      }
+    },
+  },
+};
+</script>

+ 276 - 0
src/modules/mark/components/markParam/areaCropper/AreaCropper.vue

@@ -0,0 +1,276 @@
+<template>
+  <div class="area-cropper">
+    <div class="cropper-img">
+      <img
+        v-if="paper.imgUrl"
+        ref="imgDom"
+        :src="paper.imgUrl"
+        @load="imgLoad"
+      />
+    </div>
+    <div
+      class="cropper-areas"
+      v-move-ele.prevent="{
+        moveStart: ($event) => {
+          boxMouseDown($event);
+        },
+        moveElement: (pos, $event) => {
+          boxMove($event);
+        },
+        moveStop: () => {
+          boxMoveStop();
+        },
+      }"
+    >
+      <area-item
+        v-for="area in areas"
+        :key="area.key"
+        :data="area"
+        :cur-element="curArea"
+        @del-element="removeArea"
+        @act-element="actCurArea"
+        @resize-over="modifyArea"
+      ></area-item>
+      <!-- 拖动选框 -->
+      <div
+        v-if="selectionStyles.w"
+        class="area-selection"
+        :style="{
+          width: selectionStyles.w + 'px',
+          height: selectionStyles.h + 'px',
+          top: selectionStyles.y + 'px',
+          left: selectionStyles.x + 'px',
+        }"
+      ></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import MoveEle from "./move-ele";
+import AreaItem from "./ElementItem.vue";
+import { randomCode } from "@/plugins/utils";
+
+export default {
+  name: "area-cropper",
+  components: { AreaItem },
+  directives: { MoveEle },
+  props: {
+    paper: {
+      type: Object,
+      default() {
+        return {
+          imgUrl: "",
+          areas: [],
+        };
+      },
+    },
+  },
+  data() {
+    return {
+      areas: [],
+      initArea: { id: null, i: 0, x: 0, y: 0, w: 0, h: 0, zIndex: 0 },
+      curArea: {},
+      selectionStartPos: { x: 0, y: 0 },
+      selectionStyles: { x: 0, y: 0, w: 0, h: 0 },
+      IS_SELECT_ACTION: false,
+      imgDisplayWidth: 0,
+      imgDisplayHeight: 0,
+    };
+  },
+  mounted() {
+    window.addEventListener("resize", this.windowResizeEvent);
+    document.addEventListener("keydown", this.keyEvent);
+  },
+  methods: {
+    getMax(arr) {
+      return Math.max.apply(null, arr);
+    },
+    toFixed(num, precision = 2) {
+      return num.toFixed(precision) * 1;
+    },
+    imgLoad() {
+      this.transformPicConfig(this.paper.areas);
+      this.$emit("paper-load");
+    },
+    windowResizeEvent() {
+      const { clientWidth, clientHeight } = this.$refs.imgDom;
+      const hRate = clientHeight / this.imgDisplayHeight;
+      const wRate = clientWidth / this.imgDisplayWidth;
+      this.areas = this.areas.map((area) => {
+        return Object.assign({}, area, {
+          key: `key-${randomCode()}`,
+          x: area.x * wRate,
+          y: area.y * hRate,
+          w: area.w * wRate,
+          h: area.h * hRate,
+        });
+      });
+      this.imgDisplayWidth = clientWidth;
+      this.imgDisplayHeight = clientHeight;
+    },
+    keyEvent(e) {
+      if (
+        e.code === "Delete" &&
+        !e.ctrlKey &&
+        !e.altKey &&
+        !e.shiftKey &&
+        !e.repeat
+      ) {
+        if (!this.curArea.id) return;
+        e.preventDefault();
+        this.removeArea(this.curArea);
+        return;
+      }
+    },
+    transformPicConfig(areas) {
+      const { clientWidth, clientHeight } = this.$refs.imgDom;
+
+      this.areas = areas.map((area, index) => {
+        let narea = {
+          id: `id-${randomCode()}`,
+          key: `key-${randomCode()}`,
+          zIndex: index + 99,
+        };
+        if (Object.keys(area).join("") === "i") {
+          narea = {
+            ...narea,
+            x: 0,
+            y: 0,
+            w: clientWidth,
+            h: clientHeight,
+          };
+        } else {
+          narea = {
+            ...narea,
+            x: area.x * clientWidth,
+            y: area.y * clientHeight,
+            w: area.w * clientWidth,
+            h: area.h * clientHeight,
+          };
+        }
+        return narea;
+      });
+      this.imgDisplayWidth = clientWidth;
+      this.imgDisplayHeight = clientHeight;
+    },
+    addArea(data) {
+      let area = Object.assign({}, this.initArea, data);
+      const maxZIndex = this.areas.length
+        ? this.getMax(this.areas.map((elem) => elem.zIndex))
+        : 0;
+      area.id = `id-${randomCode()}`;
+      area.key = `key-${randomCode()}`;
+      area.zIndex = maxZIndex + 1;
+      this.areas.push(area);
+      this.actCurArea(area);
+      this.emitChange();
+    },
+    modifyArea(area) {
+      const pos = this.areas.findIndex((elem) => elem.id === area.id);
+      this.areas.splice(pos, 1, area);
+      this.actCurArea(area);
+      this.emitChange();
+    },
+    removeArea(area) {
+      const pos = this.areas.findIndex((elem) => elem.id === area.id);
+      this.areas.splice(pos, 1);
+      if (this.areas.length) this.actCurArea(this.areas[0]);
+      this.emitChange();
+    },
+    actCurArea(area) {
+      this.curArea = area;
+      this.$emit("curarea-change", area);
+    },
+    curareaChange(area) {
+      if (area.id !== this.curArea.id) this.curArea = {};
+    },
+    emitChange() {
+      const { clientWidth, clientHeight } = this.$refs.imgDom;
+
+      const areas = this.areas.map((item) => {
+        return {
+          id: item.id,
+          x: this.toFixed(item.x / clientWidth, 4),
+          y: this.toFixed(item.y / clientHeight),
+          w: this.toFixed(item.w / clientWidth, 4),
+          h: this.toFixed(item.h / clientHeight),
+          // 是否覆盖整个页面,允许2px的误差
+          isFull:
+            item.x <= 2 &&
+            item.y <= 2 &&
+            item.w + 2 >= clientWidth &&
+            item.h + 2 >= clientHeight,
+        };
+      });
+
+      this.$emit("change", areas);
+    },
+    getOffsetInfo(dom, endParentClass = "cropper-areas") {
+      let parentNode = dom;
+      let parentNodeClass = parentNode.getAttribute("class") || "";
+      let offsetTop = 0,
+        offsetLeft = 0;
+      while (!parentNodeClass.includes(endParentClass)) {
+        if (parentNode.offsetParent) {
+          offsetTop += parentNode.offsetTop;
+          offsetLeft += parentNode.offsetLeft;
+          parentNode = parentNode.offsetParent;
+        } else {
+          offsetTop += parentNode.clientTop;
+          offsetLeft += parentNode.clientLeft;
+          parentNode = parentNode.parentNode;
+        }
+        parentNodeClass = parentNode.getAttribute("class") || "";
+      }
+      return {
+        offsetLeft,
+        offsetTop,
+      };
+    },
+    boxMouseDown($event) {
+      const { offsetLeft: x, offsetTop: y } = this.getOffsetInfo($event.target);
+      this.selectionStartPos.x = x + $event.offsetX;
+      this.selectionStartPos.y = y + $event.offsetY;
+    },
+    boxMove($event) {
+      const { offsetLeft: x, offsetTop: y } = this.getOffsetInfo($event.target);
+      const selectionEndPos = {
+        x: x + $event.offsetX,
+        y: y + $event.offsetY,
+      };
+      const sPos = {
+        x: Math.min(this.selectionStartPos.x, selectionEndPos.x),
+        y: Math.min(this.selectionStartPos.y, selectionEndPos.y),
+      };
+      const ePos = {
+        x: Math.max(this.selectionStartPos.x, selectionEndPos.x),
+        y: Math.max(this.selectionStartPos.y, selectionEndPos.y),
+      };
+
+      this.selectionStyles = {
+        ...sPos,
+        w: ePos.x - sPos.x,
+        h: ePos.y - sPos.y,
+      };
+      this.IS_SELECT_ACTION = true;
+    },
+    boxMoveStop() {
+      if (
+        this.IS_SELECT_ACTION &&
+        this.selectionStyles.w > 20 &&
+        this.selectionStyles.h > 20
+      )
+        this.addArea(this.selectionStyles);
+
+      this.selectionStyles = { x: 0, y: 0, w: 0, h: 0 };
+      this.selectionStartPos = { x: 0, y: 0 };
+      this.IS_SELECT_ACTION = false;
+    },
+  },
+  beforeDestroy() {
+    window.removeEventListener("resize", this.windowResizeEvent);
+    document.removeEventListener("keydown", this.keyEvent);
+  },
+};
+</script>

+ 77 - 0
src/modules/mark/components/markParam/areaCropper/ElementItem.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="element-item">
+    <element-resize
+      v-model="elemData"
+      :class="{ 'element-resize-act': curElement.id === data.id }"
+      :active="active"
+      :element-pk="data.id"
+      :style="{ zIndex: data.zIndex }"
+      isCompact
+      @resize-over="resizeOver"
+      @on-click="activeCurElement"
+    >
+      <div class="element-item-body" :style="styles" :id="data.id"></div>
+      <div class="element-delete" @click="toDelete">
+        <i class="el-icon-error"></i>
+      </div>
+    </element-resize>
+  </div>
+</template>
+
+<script>
+import ElementResize from "./ElementResize";
+import { objAssign } from "@/plugins/utils";
+
+export default {
+  name: "element-item",
+  components: {
+    ElementResize,
+  },
+  props: {
+    data: {
+      type: Object,
+    },
+    curElement: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      elemData: {
+        x: 0,
+        y: 0,
+        w: 0,
+        h: 0,
+      },
+      styles: {},
+      active: ["r", "rb", "b", "lb", "l", "lt", "t", "rt"],
+    };
+  },
+  created() {
+    this.init();
+  },
+  methods: {
+    init() {
+      this.elemData = objAssign(this.elemData, this.data);
+      this.styles = {
+        left: this.data.x + "px",
+        top: this.data.y + "px",
+        width: this.data.w + "px",
+        height: this.data.h + "px",
+      };
+    },
+    resizeOver() {
+      this.$emit("resize-over", Object.assign({}, this.data, this.elemData));
+    },
+    activeCurElement() {
+      this.$emit("act-element", this.data);
+    },
+    toDelete() {
+      this.$emit("del-element", this.data);
+    },
+  },
+};
+</script>

+ 669 - 0
src/modules/mark/components/markParam/areaCropper/ElementResize.vue

@@ -0,0 +1,669 @@
+<template>
+  <div
+    :class="classes"
+    :style="styles"
+    v-move-ele.prevent.stop="{
+      moveStart,
+      moveElement,
+      moveStop: moveElementOver,
+    }"
+  >
+    <slot></slot>
+    <div class="resize-control">
+      <div
+        v-for="(control, index) in controlPoints"
+        :key="index"
+        :class="control.classes"
+        v-move-ele.prevent.stop="{
+          moveElement: control.movePoint,
+          moveStop: control.movePointOver,
+        }"
+      ></div>
+      <div class="control-line control-line-left"></div>
+      <div class="control-line control-line-right"></div>
+      <div class="control-line control-line-top"></div>
+      <div class="control-line control-line-bottom"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import MoveEle from "./move-ele";
+
+export default {
+  name: "element-resize",
+  directives: { MoveEle },
+  props: {
+    value: {
+      type: Object,
+      required: true,
+    },
+    active: {
+      type: Array,
+      default() {
+        return ["r", "rb", "b", "lb", "l", "lt", "t", "rt"];
+      },
+    },
+    move: {
+      type: Boolean,
+      default: true,
+    },
+    minWidth: {
+      type: Number,
+      default: 60,
+      validator(val) {
+        return val >= 0;
+      },
+    },
+    maxWidth: {
+      type: Number,
+      default: 0,
+      validator(val) {
+        return val >= 0;
+      },
+    },
+    minHeight: {
+      type: Number,
+      default: 40,
+      validator(val) {
+        return val >= 0;
+      },
+    },
+    maxHeight: {
+      type: Number,
+      default: 0,
+      validator(val) {
+        return val >= 0;
+      },
+    },
+    fitParent: {
+      type: Array,
+      default() {
+        return ["w", "h"];
+      },
+    },
+    isCompact: {
+      type: Boolean,
+      default: false,
+    },
+    elementPk: {
+      type: String,
+      default: "",
+    },
+  },
+  data() {
+    return {
+      sizePosOrigin: {
+        x: 0,
+        y: 0,
+        w: 0,
+        h: 0,
+      },
+      offsetTopOrigin: 0,
+      sizePos: {
+        x: 0,
+        y: 0,
+        w: 0,
+        h: 0,
+      },
+      lastSizePos: {},
+      initOver: false,
+      controlPoints: [],
+      positionType: "static",
+      parentNodeSize: {
+        w: 0,
+        h: 0,
+      },
+      validSizePos: {},
+      points: {},
+    };
+  },
+  computed: {
+    styles() {
+      return this.initOver
+        ? {
+            left: this.sizePos.x + "px",
+            top: this.sizePos.y + "px",
+            width: this.sizePos.w + "px",
+            height: this.sizePos.h + "px",
+            position: this.positionType,
+          }
+        : {};
+    },
+    classes() {
+      return [
+        "element-resize",
+        {
+          "element-resize-move": this.move,
+          "element-resize-init": this.initOver,
+          "element-resize-compact": this.isCompact,
+        },
+      ];
+    },
+    fitParentTypeWidth() {
+      return this.fitParent.includes("w");
+    },
+    fitParentTypeHeight() {
+      return this.fitParent.includes("h");
+    },
+  },
+  created() {
+    this.initControlPoints();
+  },
+  mounted() {
+    this.initSize();
+  },
+  methods: {
+    initControlPoints() {
+      const posName = {
+        l: "Left",
+        r: "Right",
+        t: "Top",
+        b: "Bottom",
+      };
+      this.controlPoints = this.active.map((type) => {
+        const posFullName = type
+          .split("")
+          .map((item) => {
+            return posName[item];
+          })
+          .join("");
+        return {
+          classes: ["control-point", `control-point-${type}`],
+          movePoint: this[`move${posFullName}Point`],
+          movePointOver: this.moveOver,
+        };
+      });
+    },
+    initSize() {
+      const resizeDom = this.$el.childNodes[0];
+      this.positionType = window.getComputedStyle(resizeDom).position;
+      this.sizePos = { ...this.value };
+      this.lastSizePos = { ...this.value };
+      this.sizePosOrigin = { ...this.value };
+      if (this.positionType === "relative")
+        this.offsetTopOrigin = this.$el.offsetTop;
+      this.initValidSizePos();
+      this.initOver = true;
+    },
+    initValidSizePos() {
+      const s = this.sizePosOrigin;
+      const points = {
+        rt: {
+          x: s.x + s.w,
+          y: s.y,
+        },
+        lt: {
+          x: s.x,
+          y: s.y,
+        },
+        lb: {
+          x: s.x,
+          y: s.y + s.h,
+        },
+        rb: {
+          x: s.x + s.w,
+          y: s.y + s.h,
+        },
+      };
+      const action = {
+        rt: () => {
+          const point = points.rt;
+          return {
+            min: {
+              x: point.x - this.minWidth,
+              y: point.y,
+            },
+            max: {
+              x: point.x - this.maxWidth,
+              y: point.y,
+            },
+          };
+        },
+        lt: () => {
+          const point = points.lt;
+          return {
+            min: {
+              x: point.x,
+              y: point.y,
+            },
+            max: {
+              x: point.x,
+              y: point.y,
+            },
+          };
+        },
+        lb: () => {
+          const point = points.lb;
+          return {
+            min: {
+              x: point.x,
+              y: point.y - this.minHeight,
+            },
+            max: {
+              x: point.x,
+              y: point.y - this.maxHeight,
+            },
+          };
+        },
+        rb: () => {
+          const point = points.rb;
+          return {
+            min: {
+              x: point.x - this.minWidth,
+              y: point.y - this.minHeight,
+            },
+            max: {
+              x: point.x - this.maxWidth,
+              y: point.y - this.maxHeight,
+            },
+          };
+        },
+      };
+
+      this.validSizePos = {
+        rt: action.rt(),
+        lt: action.lt(),
+        lb: action.lb(),
+        rb: action.rb(),
+      };
+    },
+    fetchValidSizePos(sizePos, actionType) {
+      const staticPointConfig = {
+        left: "rt",
+        "left-bottom": "rt",
+        bottom: "lt",
+        "right-bottom": "lt",
+        right: "lb",
+        "right-top": "lb",
+        top: "rb",
+        "left-top": "rb",
+      };
+      const validSizePos = this.validSizePos[staticPointConfig[actionType]];
+
+      if (sizePos.w <= this.minWidth) {
+        sizePos.w = this.minWidth;
+        sizePos.x = validSizePos.min.x;
+      } else if (this.maxWidth !== 0 && sizePos.w >= this.maxWidth) {
+        sizePos.w = this.maxWidth;
+        sizePos.x = validSizePos.max.x;
+      }
+
+      if (sizePos.h <= this.minHeight) {
+        sizePos.h = this.minHeight;
+        sizePos.y = validSizePos.min.y;
+      } else if (this.maxHeight !== 0 && sizePos.h >= this.maxHeight) {
+        sizePos.h = this.maxHeight;
+        sizePos.y = validSizePos.max.y;
+      }
+
+      if (!this.fitParent.length) {
+        this.lastSizePos = { ...sizePos };
+        return sizePos;
+      }
+
+      // 不同的定位方式,计算方式有差异
+      this.parentNodeSize = {
+        w: this.$el.offsetParent.offsetWidth,
+        h: this.$el.offsetParent.offsetHeight,
+      };
+
+      if (this.fitParentTypeWidth) {
+        if (sizePos.x <= 0) {
+          sizePos.x = 0;
+          if (actionType.includes("left")) sizePos.w = this.lastSizePos.w;
+        }
+
+        if (sizePos.x + sizePos.w > this.parentNodeSize.w) {
+          sizePos.x = this.lastSizePos.x;
+          sizePos.w = this.parentNodeSize.w - sizePos.x;
+        }
+      }
+
+      if (this.fitParentTypeHeight) {
+        if (this.positionType === "relative") {
+          const elOffsetTop = this.$el.offsetTop;
+          if (this.sizePosOrigin.y - sizePos.y >= this.offsetTopOrigin) {
+            sizePos.h = this.lastSizePos.h;
+            sizePos.y = this.sizePosOrigin.y - this.offsetTopOrigin;
+          }
+          if (elOffsetTop + sizePos.h >= this.parentNodeSize.h) {
+            sizePos.y = this.lastSizePos.y;
+            sizePos.h = this.lastSizePos.h;
+          }
+        } else {
+          if (sizePos.y <= 0) {
+            sizePos.y = 0;
+            if (actionType.includes("top")) sizePos.h = this.lastSizePos.h;
+          }
+          if (sizePos.y + sizePos.h > this.parentNodeSize.h) {
+            sizePos.y = this.lastSizePos.y;
+            sizePos.h = this.parentNodeSize.h - sizePos.y;
+          }
+        }
+      }
+      this.lastSizePos = { ...sizePos };
+      return sizePos;
+    },
+    getLeftSize(left) {
+      return {
+        w: -left + this.sizePosOrigin.w,
+        x: left + this.sizePosOrigin.x,
+      };
+    },
+    getRightSize(left) {
+      return {
+        w: left + this.sizePosOrigin.w,
+      };
+    },
+    getTopSize(top) {
+      return {
+        h: -top + this.sizePosOrigin.h,
+        y: top + this.sizePosOrigin.y,
+      };
+    },
+    getBottomSize(top) {
+      return {
+        h: top + this.sizePosOrigin.h,
+      };
+    },
+    moveLeftPoint({ left }) {
+      console.log(this.sizePosOrigin);
+      const sp = { ...this.sizePos, ...this.getLeftSize(left) };
+      this.sizePos = { ...this.fetchValidSizePos(sp, "left") };
+      this.emitChange();
+    },
+    moveRightPoint({ left }) {
+      const sp = { ...this.sizePos, ...this.getRightSize(left) };
+      this.sizePos = { ...this.fetchValidSizePos(sp, "right") };
+      this.emitChange();
+    },
+    moveTopPoint({ top }) {
+      const sp = { ...this.sizePos, ...this.getTopSize(top) };
+      this.sizePos = { ...this.fetchValidSizePos(sp, "top") };
+      this.emitChange();
+    },
+    moveBottomPoint({ top }) {
+      const sp = { ...this.sizePos, ...this.getBottomSize(top) };
+      this.sizePos = { ...this.fetchValidSizePos(sp, "bottom") };
+      this.emitChange();
+    },
+    moveLeftTopPoint({ left, top }) {
+      const sp = {
+        ...this.sizePos,
+        ...this.getLeftSize(left),
+        ...this.getTopSize(top),
+      };
+      this.sizePos = { ...this.fetchValidSizePos(sp, "left-top") };
+      this.emitChange();
+    },
+    moveRightTopPoint({ left, top }) {
+      const sp = {
+        ...this.sizePos,
+        ...this.getRightSize(left),
+        ...this.getTopSize(top),
+      };
+      this.sizePos = { ...this.fetchValidSizePos(sp, "right-top") };
+      this.emitChange();
+    },
+    moveLeftBottomPoint({ left, top }) {
+      const sp = {
+        ...this.sizePos,
+        ...this.getLeftSize(left),
+        ...this.getBottomSize(top),
+      };
+      this.sizePos = { ...this.fetchValidSizePos(sp, "left-bottom") };
+      this.emitChange();
+    },
+    moveRightBottomPoint({ left, top }) {
+      const sp = {
+        ...this.sizePos,
+        ...this.getRightSize(left),
+        ...this.getBottomSize(top),
+      };
+      this.sizePos = { ...this.fetchValidSizePos(sp, "right-bottom") };
+      this.emitChange();
+    },
+    moveOver() {
+      this.sizePosOrigin = { ...this.sizePos };
+      this.lastSizePos = { ...this.sizePos };
+      if (this.positionType === "relative")
+        this.offsetTopOrigin = this.$el.offsetTop < 0 ? 0 : this.$el.offsetTop;
+
+      this.initValidSizePos();
+      this.$emit("resize-over", this.sizePos);
+    },
+    moveStart() {
+      this.$emit("on-click");
+    },
+    moveElement({ left, top }) {
+      if (!this.move) return;
+
+      const sp = {
+        ...this.sizePos,
+        ...{
+          x: left + this.sizePosOrigin.x,
+          y: top + this.sizePosOrigin.y,
+        },
+      };
+      this.sizePos = { ...this.fetchValidSizePos(sp, "move") };
+      this.emitChange();
+    },
+    moveElementOver() {
+      if (!this.move) return;
+      this.moveOver();
+    },
+    emitChange() {
+      this.$emit("input", this.sizePos);
+      this.$emit("change", this.sizePos);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scope>
+.element-resize {
+  position: static;
+  z-index: auto;
+  background: #fff;
+  box-sizing: content-box;
+
+  &-move {
+    cursor: move;
+  }
+
+  &-init {
+    > div:first-child {
+      width: 100% !important;
+      height: 100% !important;
+      position: relative !important;
+      top: 0 !important;
+      left: 0 !important;
+      overflow: hidden;
+    }
+  }
+  .control-point {
+    position: absolute;
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    background: #3a5ae5;
+    z-index: 99;
+    &-l {
+      left: 0;
+      top: 50%;
+      width: 5px;
+      height: 20px;
+      margin-top: -10px;
+      margin-left: -3px;
+      border-radius: 0;
+      padding-top: 3px;
+      cursor: w-resize;
+      text-align: center;
+      color: #fff;
+
+      &::before {
+        content: ".";
+        display: block;
+        font-size: 16px;
+        line-height: 1;
+        margin-top: -9px;
+      }
+      &::after {
+        content: ".";
+        display: block;
+        font-size: 16px;
+        line-height: 1;
+        margin-top: -9px;
+      }
+    }
+    &-lt {
+      left: 0;
+      top: 0;
+      margin-top: -5px;
+      margin-left: -5px;
+      cursor: nw-resize;
+    }
+    &-lb {
+      left: 0;
+      bottom: 0;
+      margin-bottom: -5px;
+      margin-left: -5px;
+      cursor: sw-resize;
+    }
+    &-r {
+      right: 0;
+      top: 50%;
+      width: 5px;
+      height: 20px;
+      margin-top: -10px;
+      margin-right: -3px;
+      cursor: e-resize;
+      border-radius: 0;
+      padding-top: 3px;
+      text-align: center;
+      color: #fff;
+
+      &::before {
+        content: ".";
+        display: block;
+        font-size: 16px;
+        line-height: 1;
+        margin-top: -9px;
+      }
+      &::after {
+        content: ".";
+        display: block;
+        font-size: 16px;
+        line-height: 1;
+        margin-top: -9px;
+      }
+    }
+    &-rt {
+      right: 0;
+      top: 0;
+      margin-top: -5px;
+      margin-right: -5px;
+      cursor: ne-resize;
+    }
+    &-rb {
+      right: 0;
+      bottom: 0;
+      margin-bottom: -5px;
+      margin-right: -5px;
+      cursor: se-resize;
+    }
+    &-t {
+      left: 50%;
+      top: 0;
+      width: 30px;
+      height: 5px;
+      border-radius: 0;
+      margin-top: -3px;
+      margin-left: -15px;
+      cursor: n-resize;
+      text-align: center;
+      color: #fff;
+      &::before {
+        content: "...";
+        display: inline-block;
+        vertical-align: top;
+        font-size: 16px;
+        line-height: 1;
+        margin-top: -10px;
+      }
+    }
+    &-b {
+      left: 50%;
+      bottom: 0;
+      width: 30px;
+      height: 5px;
+      border-radius: 0;
+      margin-bottom: -3px;
+      margin-left: -15px;
+      cursor: s-resize;
+      text-align: center;
+      color: #fff;
+
+      &::before {
+        content: "...";
+        display: inline-block;
+        vertical-align: top;
+        font-size: 16px;
+        line-height: 1;
+        margin-top: -10px;
+      }
+    }
+  }
+  .control-line {
+    position: absolute;
+    z-index: 98;
+
+    &-left {
+      height: 100%;
+      left: -1px;
+      top: 0;
+      border-left: 1px solid #3a5ae5;
+    }
+    &-right {
+      height: 100%;
+      right: -1px;
+      top: 0;
+      border-left: 1px solid #3a5ae5;
+    }
+    &-top {
+      width: 100%;
+      left: 0;
+      top: -1px;
+      border-top: 1px solid #3a5ae5;
+    }
+    &-bottom {
+      width: 100%;
+      left: 0;
+      bottom: -1px;
+      border-top: 1px solid #3a5ae5;
+    }
+  }
+
+  &-compact {
+    .control-line {
+      &-left {
+        left: 0;
+        border-left: 1px solid #bbb;
+      }
+      &-right {
+        right: 0;
+        border-left: 1px solid #bbb;
+      }
+      &-top {
+        top: 0;
+        border-top: 1px solid #bbb;
+      }
+      &-bottom {
+        bottom: 0;
+        border-top: 1px solid #bbb;
+      }
+    }
+  }
+}
+</style>

+ 49 - 0
src/modules/mark/components/markParam/areaCropper/move-ele.js

@@ -0,0 +1,49 @@
+module.exports = {
+  inserted(el, { value, modifiers }) {
+    let [_x, _y] = [0, 0];
+    // 只允许鼠标左键触发
+    let moveHandle = function (e) {
+      if (e.button !== 0) return;
+      if (modifiers.prevent) {
+        e.preventDefault();
+      }
+      if (modifiers.stop) {
+        e.stopPropagation();
+      }
+
+      let left = e.pageX - _x;
+      let top = e.pageY - _y;
+
+      value.moveElement({ left, top }, e);
+    };
+
+    let upHandle = function (e) {
+      if (e.button !== 0) return;
+      if (modifiers.prevent) {
+        e.preventDefault();
+      }
+      if (modifiers.stop) {
+        e.stopPropagation();
+      }
+      value.moveStop && value.moveStop(e);
+      document.removeEventListener("mousemove", moveHandle);
+      document.removeEventListener("mouseup", upHandle);
+    };
+
+    el.addEventListener("mousedown", function (e) {
+      if (e.button !== 0) return;
+      if (modifiers.prevent) {
+        e.preventDefault();
+      }
+      if (modifiers.stop) {
+        e.stopPropagation();
+      }
+      _x = e.pageX;
+      _y = e.pageY;
+      value.moveStart && value.moveStart(e);
+
+      document.addEventListener("mousemove", moveHandle);
+      document.addEventListener("mouseup", upHandle);
+    });
+  },
+};

+ 48 - 0
src/modules/mark/components/markParam/store.js

@@ -0,0 +1,48 @@
+const state = {
+  basicInfo: {},
+  structureCanEdit: false,
+  paperStructureInfo: [],
+  groupInfo: [],
+  openMarkClass: false,
+  classInfo: [],
+  objectiveStructure: [],
+};
+
+const mutations = {
+  setBasicInfo(state, basicInfo) {
+    state.basicInfo = basicInfo;
+  },
+  setPaperStructureInfo(state, paperStructureInfo) {
+    state.paperStructureInfo = paperStructureInfo;
+  },
+  setStructureCanEdit(state, structureCanEdit) {
+    state.structureCanEdit = structureCanEdit;
+  },
+  setGroupInfo(state, groupInfo) {
+    state.groupInfo = groupInfo;
+  },
+  setOpenMarkClass(state, openMarkClass) {
+    state.openMarkClass = openMarkClass;
+  },
+  setClassInfo(state, classInfo) {
+    state.classInfo = classInfo;
+  },
+  setObjectiveAnswerInfo(state, objectiveAnswerInfo) {
+    state.objectiveAnswerInfo = objectiveAnswerInfo;
+  },
+  initStore(state) {
+    state.basicInfo = {};
+    state.structureCanEdit = false;
+    state.paperStructureInfo = [];
+    state.groupInfo = [];
+    state.openMarkClass = false;
+    state.classInfo = [];
+    state.objectiveStructure = [];
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+};

+ 9 - 3
src/modules/mark/views/MarkSetting.vue

@@ -117,16 +117,22 @@
       :instance="curRow"
       @modified="getList"
     ></modify-mark-setting>
+    <!-- ModifyMarkParams -->
+    <modify-mark-params
+      ref="ModifyMarkParams"
+      :instance="curRow"
+    ></modify-mark-params>
   </div>
 </template>
 
 <script>
 import { markSettingListPage } from "../api";
 import ModifyMarkSetting from "../components/ModifyMarkSetting.vue";
+import ModifyMarkParams from "../components/markParam/ModifyMarkParams";
 
 export default {
   name: "mark-setting",
-  components: { ModifyMarkSetting },
+  components: { ModifyMarkSetting, ModifyMarkParams },
   data() {
     return {
       filter: {
@@ -171,8 +177,8 @@ export default {
       this.multipleSelection = val.map((item) => item.id);
     },
     toSetParams(row) {
-      // TODO:去设置参数
-      console.log(row);
+      this.curTask = row;
+      this.$refs.ModifyMarkParams.open();
     },
     toModifySetting(row) {
       this.curRow = row;