Ver código fonte

参数设置

zhangjie 3 anos atrás
pai
commit
e6b05f5e9e

BIN
public/img/card-001.jpeg


BIN
public/img/card-002.jpeg


+ 6 - 0
src/assets/styles/element-ui-costom.scss

@@ -636,3 +636,9 @@
     }
   }
 }
+// .el-tag
+.el-tag {
+  &.tag-spin {
+    margin: 3px;
+  }
+}

+ 101 - 0
src/assets/styles/pages.scss

@@ -922,3 +922,104 @@
     min-height: 300px;
   }
 }
+// modify-mark-area
+.modify-mark-area {
+  .el-dialog__body {
+    background-color: $--color-background;
+    position: relative;
+    z-index: 3;
+  }
+  .el-dialog__footer {
+    display: none;
+  }
+  .area-cropper {
+    position: relative;
+    border: 1px solid #e0e0e0;
+    background-color: #fff;
+    margin: 10px 0;
+
+    .cropper-img img {
+      display: block;
+      width: 100%;
+      height: auto;
+    }
+    .cropper-areas {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      z-index: 9;
+    }
+
+    .area-selection {
+      position: absolute;
+      z-index: 999;
+      border: 1px solid $--color-blue;
+      background-color: rgba($color: #000000, $alpha: 0.2);
+    }
+
+    .element-item-body {
+      position: absolute;
+      background-color: rgba($color: #000000, $alpha: 0.2);
+    }
+
+    .element-resize {
+      background-color: transparent;
+      > .resize-control {
+        > .control-point,
+        > .control-line {
+          display: none;
+        }
+      }
+
+      &:hover {
+        > .resize-control {
+          > .control-line {
+            display: block;
+          }
+        }
+      }
+
+      &-act {
+        > .resize-control {
+          > .control-point,
+          > .control-line {
+            display: block;
+          }
+        }
+      }
+    }
+    .element-resize-compact {
+      > .resize-control {
+        > .control-line {
+          display: block;
+        }
+      }
+
+      &:hover {
+        > .resize-control {
+          > .control-line {
+            border-color: #4794b3;
+          }
+        }
+      }
+
+      &.element-resize-act {
+        > .resize-control {
+          > .control-line {
+            border-color: #4794b3;
+            &-left,
+            &-right {
+              border-left-style: solid;
+            }
+            &-top,
+            &-bottom {
+              border-top-style: solid;
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 7 - 0
src/modules/stmms/api.js

@@ -9,6 +9,13 @@ export const examStructureUpload = datas => {
 export const examStructurePreviewStructure = id => {
   return $postParam("/api/admin/exam/structure/preview_structure", { id });
 };
+export const examStructureFindJpg = datas => {
+  return $postParam("/api/admin/exam/structure/find_jpg_file", datas);
+};
+export const examStructureSubmit = datas => {
+  return $post("/api/admin/exam/structure/submit", datas);
+};
+
 // score-archive
 export const scoreListPage = datas => {
   return $postParam("/api/admin/sync/score/list", datas);

+ 0 - 202
src/modules/stmms/components/MarkPaperMarker.vue

@@ -1,202 +0,0 @@
-<template>
-  <div class="mark-paper-marker">
-    <div class="marker-header">
-      <p
-        class="marker-desc color-danger"
-        v-if="questionCount > markerQuestionCount"
-      >
-        本试卷共<span class="mlr-1">{{ questionCount }}</span
-        >道小题,已经设置<span class="mlr-1">{{ markerQuestionCount }}</span
-        >道,还有<span class="mlr-1">{{
-          questionCount - markerQuestionCount
-        }}</span
-        >道未设置评卷员,请继续设置,确保全部题目均已分配评卷员!
-      </p>
-      <p class="marker-desc color-success" v-else>
-        本试卷共<span class="mlr-1">{{ questionCount }}</span
-        >道小题,已全部设置评卷员!
-      </p>
-      <el-button type="primary" @click="toAdd">新增</el-button>
-    </div>
-
-    <el-table :data="markers" border>
-      <el-table-column type="index" width="50"> </el-table-column>
-      <el-table-column label="评卷员"></el-table-column>
-      <el-table-column label="评卷方式">
-        <template slot-scope="scope">
-          <el-radio-group v-model="scope.row.markType">
-            <el-radio
-              v-for="(val, key) in MARK_TYPE"
-              :key="key"
-              :label="key * 1"
-              >{{ val }}</el-radio
-            >
-          </el-radio-group>
-          <span v-if="scope.row.markType === 2">
-            仲裁阀值:<el-input-number
-              v-model="scope.row.arbitration"
-              class="width-80"
-              size="small"
-              :min="0.1"
-              :max="100"
-              :step="0.1"
-              step-strictly
-              :controls="false"
-            >
-            </el-input-number>
-          </span>
-        </template>
-      </el-table-column>
-      <el-table-column label="评阅题目">
-        <template slot-scope="scope">
-          {{ scope.row.questions | questionsFilter }}
-        </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-danger" type="text" @click="toDelete(scope.row)"
-            >删除</el-button
-          >
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <!-- ModifyMarkerQuestion -->
-    <modify-marker-question
-      ref="ModifyMarkerQuestion"
-      :instance="curMarker"
-      :disabled-question-ids="disabledQuestionIds"
-      :paper-structure="datas.structure"
-      @modified="modified"
-    ></modify-marker-question>
-  </div>
-</template>
-
-<script>
-import ModifyMarkerQuestion from "./ModifyMarkerQuestion";
-
-export default {
-  name: "mark-paper-marker",
-  components: { ModifyMarkerQuestion },
-  props: {
-    datas: {
-      type: Object,
-      default() {
-        return {
-          task: {},
-          structure: [],
-          markers: []
-        };
-      }
-    }
-  },
-  data() {
-    return {
-      questionCount: 0,
-      markerQuestionCount: 0,
-      markers: [],
-      disabledQuestionIds: [],
-      curMarker: {},
-      MARK_TYPE: {
-        1: "单评",
-        2: "双评"
-      }
-    };
-  },
-  filters: {
-    questionsFilter(val) {
-      return val.map(item => `${item.mainNumber}-${item.subNumber}`).join(",");
-    }
-  },
-  mounted() {
-    this.initData();
-  },
-  methods: {
-    initData() {
-      this.markers = this.datas.markers.map(item => {
-        return { ...item };
-      });
-      this.questionCount = this.datas.structure.length;
-      this.updateDisableQuestionIds();
-
-      this.$emit("on-ready");
-    },
-    updateDisableQuestionIds(filterId) {
-      let markers = this.markers;
-      if (filterId) markers = this.markers.filter(item => item.id !== filterId);
-      let disabledQuestionIds = [];
-      markers.forEach(item => {
-        disabledQuestionIds = [
-          ...disabledQuestionIds,
-          ...item.questions.map(item => item.id)
-        ];
-      });
-      this.disabledQuestionIds = disabledQuestionIds;
-      if (!filterId) this.markerQuestionCount = disabledQuestionIds.length;
-    },
-    toAdd() {
-      this.updateDisableQuestionIds();
-      if (this.markerQuestionCount === this.questionCount) {
-        this.$message.error("当前已经没有题目可供设置!");
-        return;
-      }
-
-      this.curMarker = {
-        id: this.$randomCode(),
-        markers: [],
-        markType: 1,
-        arbitration: 1,
-        questions: []
-      };
-      this.$refs.ModifyMarkerQuestion.open();
-    },
-    toEdit(row) {
-      this.curMarker = row;
-      this.updateDisableQuestionIds(row.id);
-      this.$refs.ModifyMarkerQuestion.open();
-    },
-    toDelete(row) {
-      const pos = this.markers.findIndex(item => item.id === row.id);
-      this.markers.splice(pos, 1);
-    },
-    modified(row) {
-      const pos = this.markers.findIndex(item => item.id === row.id);
-      this.markers.splice(pos, 1, row);
-      this.updateDisableQuestionIds();
-    },
-    checkData() {
-      let errorMessages = [];
-
-      if (this.questionCount > this.markerQuestionCount) {
-        errorMessages.push("当前还有题目未设置评卷员");
-      }
-
-      this.markers.forEach((item, index) => {
-        if (item.markType === 2 && !item.arbitration) {
-          errorMessages.push(`序号${index + 1}设置中,仲裁阀值不能为空`);
-        }
-      });
-
-      if (errorMessages.length) {
-        this.$message.error(errorMessages.join("。"));
-        this.$emit("on-ready");
-        return;
-      }
-
-      this.updateData();
-      this.$emit("next-step");
-    },
-    getData() {
-      return this.markers.map(item => {
-        return { ...item };
-      });
-    },
-    updateData() {
-      this.$emit("data-change", { markers: this.getData() });
-    }
-  }
-};
-</script>

+ 264 - 0
src/modules/stmms/components/markParam/MarkPaperMarker.vue

@@ -0,0 +1,264 @@
+<template>
+  <div class="mark-paper-marker">
+    <div class="marker-header">
+      <p
+        class="marker-desc color-danger"
+        v-if="subjectiveQuestionCount > groupQuestionCount"
+      >
+        本试卷共<span class="mlr-1">{{ questionCount }}</span
+        >道小题,客观题<span class="mlr-1">{{ objectiveQuestionCount }}</span
+        >道,主观题<span class="mlr-1">{{ subjectiveQuestionCount }}</span
+        >道,已经设置<span class="mlr-1">{{ groupQuestionCount }}</span
+        >道主观题,还有<span class="mlr-1">{{
+          subjectiveQuestionCount - groupQuestionCount
+        }}</span
+        >道主观题未设置评卷员,请继续设置,确保全部主观题均已分配评卷员!
+      </p>
+      <p class="marker-desc color-success" v-else>
+        本试卷共<span class="mlr-1">{{ questionCount }}</span
+        >道小题,客观题<span class="mlr-1">{{ objectiveQuestionCount }}</span
+        >道,主观题<span class="mlr-1">{{ subjectiveQuestionCount }}</span
+        >道,主观题已全部设置评卷员!
+      </p>
+      <el-button type="primary" @click="toAdd">新增</el-button>
+    </div>
+
+    <el-table :data="groupInfo" 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.label }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="评卷方式">
+        <template slot-scope="scope">
+          <el-radio-group v-model="scope.row.doubleRate">
+            <el-radio
+              v-for="(val, key) in MARK_TYPE"
+              :key="key"
+              :label="key * 1"
+              >{{ val }}</el-radio
+            >
+          </el-radio-group>
+          <span v-if="scope.row.doubleRate === 1">
+            仲裁阀值:<el-input-number
+              v-model="scope.row.arbitrateThreshold"
+              class="width-80"
+              size="small"
+              :min="0"
+              :max="999999"
+              :step="0.01"
+              step-strictly
+              :controls="false"
+            >
+            </el-input-number>
+          </span>
+        </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.pictureConfigList.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"
+            @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>
+
+    <!-- ModifyMarkerQuestion -->
+    <modify-marker-question
+      ref="ModifyMarkerQuestion"
+      :instance="curGroupInfo"
+      :disabled-question-ids="disabledQuestionIds"
+      :paper-structure="subjectiveQuestionList"
+      @modified="groupModified"
+    ></modify-marker-question>
+    <!-- ModifyMarkArea -->
+    <modify-mark-area
+      ref="ModifyMarkArea"
+      :base-info="datas.basicPaperInfo"
+      :group="curGroupInfo"
+      @modified="areaModified"
+    ></modify-mark-area>
+  </div>
+</template>
+
+<script>
+import ModifyMarkerQuestion from "./ModifyMarkerQuestion";
+import ModifyMarkArea from "./ModifyMarkArea.vue";
+
+export default {
+  name: "mark-paper-marker",
+  components: { ModifyMarkerQuestion, ModifyMarkArea },
+  props: {
+    datas: {
+      type: Object,
+      default() {
+        return {
+          basicPaperInfo: {},
+          paperStructureInfo: [],
+          groupInfo: []
+        };
+      }
+    }
+  },
+  data() {
+    return {
+      questionCount: 0,
+      groupQuestionCount: 0,
+      groupInfo: [],
+      disabledQuestionIds: [],
+      curGroupInfo: {},
+      subjectiveQuestionList: [],
+      subjectiveQuestionCount: 0,
+      objectiveQuestionCount: 0,
+      MARK_TYPE: {
+        0: "单评",
+        1: "双评"
+      }
+    };
+  },
+  filters: {
+    questionsFilter(val) {
+      return val.map(item => `${item.mainNumber}-${item.subNumber}`).join(",");
+    }
+  },
+  mounted() {
+    this.initData();
+  },
+  methods: {
+    initData() {
+      this.groupInfo = this.datas.groupInfo.map(item => {
+        return { ...item };
+      });
+      this.questionCount = this.datas.paperStructureInfo.length;
+      this.updateDisableQuestionIds();
+
+      this.subjectiveQuestionList = this.datas.paperStructureInfo.filter(
+        item => item.qType === "subjective"
+      );
+      this.subjectiveQuestionCount = this.subjectiveQuestionList.length;
+      this.objectiveQuestionCount =
+        this.questionCount - this.subjectiveQuestionCount;
+
+      this.$emit("on-ready");
+    },
+    updateDisableQuestionIds(filterId) {
+      let groupInfo = this.groupInfo;
+      if (filterId)
+        groupInfo = this.groupInfo.filter(item => item.id !== filterId);
+      let disabledQuestionIds = [];
+      groupInfo.forEach(item => {
+        disabledQuestionIds = [
+          ...disabledQuestionIds,
+          ...item.questions.map(item => item.id)
+        ];
+      });
+      this.disabledQuestionIds = disabledQuestionIds;
+      if (!filterId) this.groupQuestionCount = disabledQuestionIds.length;
+    },
+    toAdd() {
+      this.updateDisableQuestionIds();
+      if (this.groupQuestionCount === this.subjectiveQuestionCount) {
+        this.$message.error("当前已经没有主观题目可供设置!");
+        return;
+      }
+
+      this.curGroupInfo = {
+        id: this.$randomCode(),
+        markerList: [],
+        doubleRate: 1,
+        arbitrateThreshold: 1,
+        questions: [],
+        pictureConfigList: []
+      };
+      this.$refs.ModifyMarkerQuestion.open();
+    },
+    toEdit(row) {
+      this.curGroupInfo = row;
+      this.updateDisableQuestionIds(row.id);
+      this.$refs.ModifyMarkerQuestion.open();
+    },
+    toSetArea(row) {
+      this.curGroupInfo = row;
+      this.$refs.ModifyMarkArea.open();
+    },
+    toDelete(row) {
+      const pos = this.groupInfo.findIndex(item => item.id === row.id);
+      this.groupInfo.splice(pos, 1);
+    },
+    groupModified(row) {
+      const pos = this.groupInfo.findIndex(item => item.id === row.id);
+      if (pos === -1) {
+        this.groupInfo.push(row);
+      } else {
+        this.groupInfo.splice(pos, 1, row);
+      }
+      this.updateDisableQuestionIds();
+    },
+    areaModified(row) {
+      const pos = this.groupInfo.findIndex(item => item.id === row.id);
+      if (pos === -1) return;
+      this.groupInfo.splice(pos, 1, row);
+    },
+    checkData() {
+      let errorMessages = [];
+
+      if (this.subjectiveQuestionCount > this.groupQuestionCount) {
+        errorMessages.push("当前还有题目未设置评卷员");
+      }
+
+      this.groupInfo.forEach((item, index) => {
+        if (item.doubleRate === 1 && !item.arbitrateThreshold) {
+          errorMessages.push(`序号${index + 1}设置中,仲裁阀值不能为空`);
+        }
+      });
+
+      if (errorMessages.length) {
+        this.$message.error(errorMessages.join("。"));
+        this.$emit("on-ready");
+        return;
+      }
+
+      this.updateData();
+      this.$emit("next-step");
+    },
+    getData() {
+      return this.groupInfo.map(item => {
+        return { ...item };
+      });
+    },
+    updateData() {
+      this.$emit("data-change", { groupInfo: this.getData() });
+    }
+  }
+};
+</script>

+ 63 - 14
src/modules/stmms/components/MarkPaperStructure.vue → src/modules/stmms/components/markParam/MarkPaperStructure.vue

@@ -2,9 +2,9 @@
   <div class="mark-paper-structure">
     <p class="structure-desc">
       <span>课程名称:</span>
-      <span class="mr-4">{{ datas.task.courseName }}</span>
+      <span class="mr-4">{{ datas.basicPaperInfo.courseName }}</span>
       <span>课程代码:</span>
-      <span>{{ datas.task.courseCode }}</span>
+      <span>{{ datas.basicPaperInfo.courseCode }}</span>
     </p>
     <el-table
       ref="TableList"
@@ -32,11 +32,28 @@
           <el-input
             v-model.trim="scope.row.mainTitle"
             size="small"
-            :maxlength="50"
+            :maxlength="200"
+            clearable
             @change="mainTitleChange(scope.row)"
           ></el-input>
         </span>
       </el-table-column>
+      <el-table-column prop="qType" label="类型" width="160">
+        <template slot-scope="scope" v-if="scope.row.isMainFirstSub">
+          <el-radio-group
+            v-model="scope.row.qType"
+            size="mini"
+            @change="qTypeChange(scope.row)"
+          >
+            <el-radio-button
+              v-for="(val, key) in Q_TYPE"
+              :key="key"
+              :label="key"
+              >{{ val }}</el-radio-button
+            >
+          </el-radio-group>
+        </template>
+      </el-table-column>
       <el-table-column prop="mainNumber" label="大题号" width="80">
         <template slot-scope="scope" v-if="scope.row.isMainFirstSub">
           <span>{{ scope.row.mainNumber }}</span>
@@ -54,7 +71,7 @@
             class="width-80"
             size="small"
             :min="0.5"
-            :max="200"
+            :max="500"
             :step="0.5"
             step-strictly
             :controls="false"
@@ -90,7 +107,7 @@
 </template>
 
 <script>
-import { calcSum } from "../../../plugins/utils";
+import { calcSum } from "@/plugins/utils";
 
 export default {
   name: "mark-paper-structure",
@@ -99,16 +116,20 @@ export default {
       type: Object,
       default() {
         return {
-          task: {},
-          structure: [],
-          markers: []
+          basicPaperInfo: {},
+          paperStructureInfo: [],
+          groupInfo: []
         };
       }
     }
   },
   data() {
     return {
-      tableData: []
+      tableData: [],
+      Q_TYPE: {
+        objective: "客观题",
+        subjective: "主观题"
+      }
     };
   },
   mounted() {
@@ -116,16 +137,20 @@ export default {
   },
   methods: {
     initData() {
-      this.tableData = this.datas.structure.map(item => {
+      this.tableData = this.datas.paperStructureInfo.map(item => {
         return { ...item };
       });
-      if (!this.tableData.length) this.createMain();
+      if (!this.tableData.length) {
+        this.createMain();
+      }
 
       this.$emit("on-ready");
     },
+
     createMain() {
       this.tableData.push({
         id: this.$randomCode(),
+        qType: "objective",
         mainId: this.$randomCode(),
         mainTitle: "",
         mainNumber: 1,
@@ -156,6 +181,7 @@ export default {
 
       this.tableData.splice(nextMainStartPos, 0, {
         id: this.$randomCode(),
+        qType: row.qType,
         mainId: this.$randomCode(),
         mainTitle: "",
         mainNumber: row.mainNumber + 1,
@@ -181,6 +207,7 @@ export default {
       const subPos = this.tableData.findIndex(item => item.id === row.id);
       this.tableData.splice(subPos + 1, 0, {
         id: this.$randomCode(),
+        qType: row.qType,
         mainId: row.mainId,
         mainTitle: row.mainTitle,
         mainNumber: row.mainNumber,
@@ -221,14 +248,37 @@ export default {
         .filter(item => item.mainId === row.mainId && !item.isMainFirstSub)
         .forEach(item => (item.mainTitle = row.mainTitle));
     },
+    qTypeChange(row) {
+      this.tableData
+        .filter(item => item.mainId === row.mainId && !item.isMainFirstSub)
+        .forEach(item => (item.qType = row.qType));
+    },
     questionScoreChange() {
       this.paperTotalScore = calcSum(
         this.tableData.map(item => item.totalScore || 0)
       );
     },
     checkData() {
-      let errorMessages = [];
+      if (
+        this.tableData.some(item => item.qType === "objective") &&
+        this.tableData[0].qType !== "objective"
+      ) {
+        this.$message.error("请保持客观题在前,主观题在后!");
+        return;
+      }
 
+      const objectiveNos = this.tableData
+        .filter(item => item.isMainFirstSub && item.qType === "objective")
+        .map(item => item.mainNumber);
+      const unValid = objectiveNos.some(
+        (no, index) => objectiveNos[0] + index !== no
+      );
+      if (unValid) {
+        this.$message.error("请保持主客观题题号连续");
+        return;
+      }
+
+      let errorMessages = [];
       this.tableData.forEach(item => {
         let errorMsg = ``;
         if (item.isMainFirstSub) {
@@ -265,7 +315,6 @@ export default {
 
       if (errorMessages.length) {
         this.$message.error(errorMessages.join("。"));
-        this.$emit("on-ready");
         return;
       }
 
@@ -278,7 +327,7 @@ export default {
       });
     },
     updateData() {
-      this.$emit("data-change", { structure: this.getData() });
+      this.$emit("data-change", { paperStructureInfo: this.getData() });
     }
   }
 };

+ 126 - 0
src/modules/stmms/components/markParam/ModifyMarkArea.vue

@@ -0,0 +1,126 @@
+<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 v-if="modalIsShow" class="area-container">
+      <area-cropper
+        v-for="(paper, index) in paperList"
+        :imgUrl="paper.imgUrl"
+        ref="AreaCropper"
+        :key="paper.imgUrl"
+        :paper="paper"
+        @curarea-change="cropperCurareaChange"
+        @change="areas => areaChange(index, areas)"
+      ></area-cropper>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import AreaCropper from "./areaCropper/AreaCropper.vue";
+import { examStructureFindJpg } from "../../api";
+
+export default {
+  name: "modify-mark-area",
+  components: { AreaCropper },
+  props: {
+    baseInfo: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    group: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      paperList: [
+        { imgUrl: "/img/card-001.jpeg", areas: [] },
+        { imgUrl: "/img/card-002.jpeg", areas: [] }
+      ]
+    };
+  },
+  mounted() {
+    this.getPaperList();
+  },
+  methods: {
+    visibleChange() {
+      this.paperList.forEach(paper => (paper.areas = []));
+      this.group.pictureConfigList.forEach(config => {
+        const index = config.i - 1;
+        this.paperList[index].areas.push({ ...config });
+      });
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async getPaperList() {
+      const data = await examStructureFindJpg({
+        examId: this.baseInfo.examId,
+        courseCode: this.baseInfo.courseCode,
+        paperNumber: this.baseInfo.paperNumber
+      });
+      const papers = data || [];
+      papers.sort((a, b) => a.index - b.index);
+      this.paperList = papers.map(paper => {
+        return {
+          imgUrl: paper.path,
+          areas: []
+        };
+      });
+    },
+    areaChange(index, areas) {
+      this.paperList[index].areas = areas;
+    },
+    cropperCurareaChange(area) {
+      this.$refs.AreaCropper.forEach(cropper => {
+        cropper.curareaChange(area);
+      });
+    },
+    confirm() {
+      let areas = [];
+      this.paperList.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, { pictureConfigList: areas })
+      );
+      this.cancel();
+    }
+  }
+};
+</script>

+ 100 - 9
src/modules/stmms/components/ModifyMarkParams.vue → src/modules/stmms/components/markParam/ModifyMarkParams.vue

@@ -65,6 +65,8 @@
 <script>
 import MarkPaperMarker from "./MarkPaperMarker.vue";
 import MarkPaperStructure from "./MarkPaperStructure.vue";
+// import paramData from "./paramData";
+import { examStructureSubmit } from "../../api";
 
 const STEPS_LIST = [
   {
@@ -94,9 +96,9 @@ export default {
     return {
       modalIsShow: false,
       infos: {
-        task: {},
-        structure: [],
-        markers: []
+        basicPaperInfo: {},
+        paperStructureInfo: [],
+        groupInfo: []
       },
       // step
       steps: STEPS_LIST,
@@ -121,11 +123,28 @@ export default {
   },
   methods: {
     visibleChange() {
-      this.dataReady = false;
       this.current = 0;
       this.loading = false;
 
-      this.infos.task = { ...this.instance };
+      if (this.instance.paperInfoJson) {
+        const paperInfoJson = JSON.parse(this.instance.paperInfoJson);
+        this.infos = {
+          paperStructureInfo: [
+            ...paperInfoJson.objectiveQuestionList,
+            ...paperInfoJson.subjectiveQuestionList
+          ],
+          groupInfo: paperInfoJson.groupInfo,
+          basicPaperInfo: { ...this.instance }
+        };
+      } else {
+        this.infos = {
+          paperStructureInfo: [],
+          groupInfo: [],
+          basicPaperInfo: { ...this.instance }
+        };
+      }
+      // this.current = 1;
+      // this.infos = paramData;
 
       this.dataReady = true;
     },
@@ -161,7 +180,6 @@ export default {
       }
     },
     nextStep() {
-      this.loading = true;
       this.$refs[this.currentComponent].checkData();
     },
     toNext() {
@@ -171,16 +189,89 @@ export default {
         this.current += 1;
       }
     },
+    getPaperStructData(paperStructureInfo) {
+      let originStruct = [];
+      let group = [];
+      let curMainId = null;
+      paperStructureInfo.forEach(item => {
+        if (curMainId !== item.mainId) {
+          curMainId = item.mainId;
+          if (group.length) originStruct.push(group.length);
+          group = [];
+        }
+        group.push(item);
+      });
+      if (group.length) originStruct.push(group.length);
+      return originStruct;
+    },
     dataChange(data) {
-      // console.log(data);
+      if (data.paperStructureInfo && this.infos.paperStructureInfo.length) {
+        // 检验试卷结构是否有变化
+        const originStruct = this.getPaperStructData(
+          this.infos.paperStructureInfo
+        );
+        const curStruct = this.getPaperStructData(data.paperStructureInfo);
+        if (curStruct.join("") !== originStruct.join("")) {
+          data.groupInfo = [];
+          this.$message.warning("试卷结构有变动,评卷员将被清空,请重新设置!");
+        } else {
+          // 更新分组中的试题信息
+          const paperMap = {};
+          data.paperStructureInfo.forEach(item => {
+            paperMap[`${item.mainNumber}-${item.subNumber}`] = item;
+          });
+          const groupInfo = this.infos.groupInfo.map(group => {
+            let ngroup = { ...group };
+            ngroup.questions = ngroup.questions.map(q => {
+              return { ...paperMap[`${q.mainNumber}-${q.subNumber}`] };
+            });
+            return ngroup;
+          });
+          data.groupInfo = groupInfo;
+        }
+      }
+
       Object.entries(data).forEach(([key, val]) => {
-        this.infos[key] = Object.assign(this.infos[key], val);
+        this.infos[key] = val;
       });
     },
     compReady(type = false) {
       this.loading = type;
     },
-    submit() {}
+    async submit() {
+      if (this.loading) return;
+      this.loading = true;
+
+      let subjectiveStartPos = this.infos.paperStructureInfo.findIndex(
+        item => item.qType === "subjective"
+      );
+      if (subjectiveStartPos === -1) {
+        subjectiveStartPos = this.infos.paperStructureInfo.length;
+      }
+      const datas = {
+        basicPaperInfo: this.infos.basicPaperInfo,
+        paperStructureInfo: {
+          objectiveQuestionList: this.infos.paperStructureInfo.slice(
+            0,
+            subjectiveStartPos
+          ),
+          subjectiveQuestionList: this.infos.paperStructureInfo.slice(
+            subjectiveStartPos
+          )
+        },
+        groupInfo: this.infos.groupInfo
+      };
+      console.log(JSON.stringify(datas));
+
+      const res = await examStructureSubmit(datas).catch(() => false);
+      this.loading = false;
+
+      if (!res) return;
+
+      this.$message.success("提交成功!");
+      this.$emit("modified");
+      this.cancel();
+    }
   }
 };
 </script>

+ 20 - 5
src/modules/stmms/components/ModifyMarkerQuestion.vue → src/modules/stmms/components/markParam/ModifyMarkerQuestion.vue

@@ -119,7 +119,7 @@
 </template>
 
 <script>
-import { organizationList } from "../../base/api";
+import { organizationList } from "../../../base/api";
 
 export default {
   name: "modify-marker-question",
@@ -199,10 +199,10 @@ export default {
     visibleChange() {
       this.parseStructs();
       this.filterLabel = "";
-      this.selectedUsers = this.instance.markers;
       this.selectedQuestions = this.instance.questions;
-      this.selectedUserIds = this.selectedUsers.map(item => item.id);
       this.selectedQuestionIds = this.selectedQuestions.map(item => item.id);
+      this.selectedUserIds = this.instance.markerList.map(item => item.id);
+      this.updateSelectedUsersFromUserIds();
       this.labelChange();
     },
     cancel() {
@@ -242,6 +242,7 @@ export default {
                 label: user.realName,
                 name: user.realName,
                 orgName: item.name,
+                loginName: item.loginName,
                 selected: false,
                 isUser: true
               };
@@ -361,6 +362,7 @@ export default {
         });
         this.selectedUserIds = this.selectedUsers.map(item => item.id);
       }
+      this.updateSelectedUsersFromUserIds();
     },
     toDeleteUser(user) {
       const pos = this.selectedUsers.findIndex(item => item.id === user.id);
@@ -424,8 +426,21 @@ export default {
       if (!valid) return;
 
       let datas = { ...this.instance };
-      datas.markers = this.selectedUsers;
-      datas.questions = this.selectedQuestions;
+      datas.markerList = this.selectedUsers.map(item => {
+        return {
+          id: item.id,
+          name: item.name,
+          loginName: item.loginName,
+          label: item.label
+        };
+      });
+      datas.questions = this.selectedQuestions.map(item => {
+        let nitem = { ...item };
+        delete nitem.disabled;
+        delete nitem.selected;
+        return nitem;
+      });
+      console.log(datas);
       this.$emit("modified", datas);
       this.cancel();
     }

+ 285 - 0
src/modules/stmms/components/markParam/areaCropper/AreaCropper.vue

@@ -0,0 +1,285 @@
+<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"
+        @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) {
+      return num.toFixed(2) * 1;
+    },
+    imgLoad() {
+      this.transformPicConfig(this.paper.areas);
+    },
+    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 {
+        naturalWidth,
+        naturalHeight,
+        clientWidth,
+        clientHeight
+      } = this.$refs.imgDom;
+      const hRate = clientHeight / naturalHeight;
+      const wRate = clientWidth / naturalWidth;
+      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 * wRate,
+            y: area.y * hRate,
+            w: area.w * wRate,
+            h: area.h * hRate
+          };
+        }
+        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);
+    },
+    actCurArea(area) {
+      this.curArea = area;
+      this.$emit("curarea-change", area);
+    },
+    curareaChange(area) {
+      if (area.id !== this.curArea.id) this.curArea = {};
+    },
+    emitChange() {
+      const {
+        naturalWidth,
+        naturalHeight,
+        clientWidth,
+        clientHeight
+      } = this.$refs.imgDom;
+      const hRate = naturalHeight / clientHeight;
+      const wRate = naturalWidth / clientWidth;
+
+      const areas = this.areas.map(item => {
+        return {
+          id: item.id,
+          x: this.toFixed(item.x * wRate),
+          y: this.toFixed(item.y * hRate),
+          w: this.toFixed(item.w * wRate),
+          h: this.toFixed(item.h * hRate),
+          // 是否覆盖整个页面,允许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>

+ 71 - 0
src/modules/stmms/components/markParam/areaCropper/ElementItem.vue

@@ -0,0 +1,71 @@
+<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>
+    </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);
+    }
+  }
+};
+</script>

+ 669 - 0
src/modules/stmms/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: #00a2fe;
+    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 #4794b3;
+    }
+    &-right {
+      height: 100%;
+      right: -1px;
+      top: 0;
+      border-left: 1px solid #4794b3;
+    }
+    &-top {
+      width: 100%;
+      left: 0;
+      top: -1px;
+      border-top: 1px solid #4794b3;
+    }
+    &-bottom {
+      width: 100%;
+      left: 0;
+      bottom: -1px;
+      border-top: 1px solid #4794b3;
+    }
+  }
+
+  &-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/stmms/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);
+    });
+  }
+};

+ 173 - 0
src/modules/stmms/components/markParam/paramData.js

@@ -0,0 +1,173 @@
+export default {
+  paperStructureInfo: [
+    {
+      id: "7b66fero9p9labg8",
+      qType: "subjective",
+      mainId: "8j3dsapo3k2gs0ao",
+      mainTitle: "1111",
+      mainNumber: 1,
+      subNumber: 1,
+      totalScore: 1,
+      isMainFirstSub: true,
+      expandSub: true
+    },
+    {
+      id: "1qn4giooe61tp9bg",
+      qType: "subjective",
+      mainId: "8j3dsapo3k2gs0ao",
+      mainTitle: "1111",
+      mainNumber: 1,
+      subNumber: 2,
+      totalScore: 1,
+      isMainFirstSub: false,
+      expandSub: true
+    },
+    {
+      id: "835rbhgosr75hgfq",
+      qType: "subjective",
+      mainId: "8j3dsapo3k2gs0ao",
+      mainTitle: "1111",
+      mainNumber: 1,
+      subNumber: 3,
+      totalScore: 1,
+      isMainFirstSub: false,
+      expandSub: true
+    },
+    {
+      id: "07pnh1iqn85sabdg",
+      qType: "subjective",
+      mainId: "9mpi7el81lefm1kj",
+      mainTitle: "2222",
+      mainNumber: 2,
+      subNumber: 1,
+      totalScore: 1,
+      isMainFirstSub: true,
+      expandSub: true
+    },
+    {
+      id: "laitpjllpmg59hhg",
+      qType: "subjective",
+      mainId: "9mpi7el81lefm1kj",
+      mainTitle: "2222",
+      mainNumber: 2,
+      subNumber: 2,
+      totalScore: 1,
+      isMainFirstSub: false,
+      expandSub: true
+    },
+    {
+      id: "deqv73p8qfh0tm68",
+      qType: "subjective",
+      mainId: "9mpi7el81lefm1kj",
+      mainTitle: "2222",
+      mainNumber: 2,
+      subNumber: 3,
+      totalScore: 1,
+      isMainFirstSub: false,
+      expandSub: true
+    }
+  ],
+  groupInfo: [
+    {
+      id: "aaklqjnok11cf4jo",
+      markerList: [
+        { id: "239689678024867840", name: "kw05", label: "kw05(艺术教研室)" },
+        { id: "239689583325872128", name: "mt05", label: "mt05(艺术教研室)" },
+        { id: "239689762452013056", name: "zr05", label: "zr05(艺术教研室)" }
+      ],
+      doubleRate: 1,
+      arbitrateThreshold: 1,
+      questions: [
+        {
+          id: "7b66fero9p9labg8",
+          qType: "subjective",
+          mainId: "8j3dsapo3k2gs0ao",
+          mainTitle: "1111",
+          mainNumber: 1,
+          subNumber: 1,
+          totalScore: 1,
+          isMainFirstSub: true,
+          expandSub: true
+        },
+        {
+          id: "1qn4giooe61tp9bg",
+          qType: "subjective",
+          mainId: "8j3dsapo3k2gs0ao",
+          mainTitle: "1111",
+          mainNumber: 1,
+          subNumber: 2,
+          totalScore: 1,
+          isMainFirstSub: false,
+          expandSub: true
+        },
+        {
+          id: "835rbhgosr75hgfq",
+          qType: "subjective",
+          mainId: "8j3dsapo3k2gs0ao",
+          mainTitle: "1111",
+          mainNumber: 1,
+          subNumber: 3,
+          totalScore: 1,
+          isMainFirstSub: false,
+          expandSub: true
+        }
+      ],
+      picConfig: []
+    },
+    {
+      id: "ddqkaotgp9gjcg38",
+      markerList: [
+        { id: "236486056571052032", name: "test1", label: "test1(美术印刷室)" },
+        { id: "236519144445444096", name: "ys01", label: "ys01(印刷室2)" }
+      ],
+      doubleRate: 1,
+      arbitrateThreshold: 1,
+      questions: [
+        {
+          id: "07pnh1iqn85sabdg",
+          qType: "subjective",
+          mainId: "9mpi7el81lefm1kj",
+          mainTitle: "2222",
+          mainNumber: 2,
+          subNumber: 1,
+          totalScore: 1,
+          isMainFirstSub: true,
+          expandSub: true
+        },
+        {
+          id: "laitpjllpmg59hhg",
+          qType: "subjective",
+          mainId: "9mpi7el81lefm1kj",
+          mainTitle: "2222",
+          mainNumber: 2,
+          subNumber: 2,
+          totalScore: 1,
+          isMainFirstSub: false,
+          expandSub: true
+        },
+        {
+          id: "deqv73p8qfh0tm68",
+          qType: "subjective",
+          mainId: "9mpi7el81lefm1kj",
+          mainTitle: "2222",
+          mainNumber: 2,
+          subNumber: 3,
+          totalScore: 1,
+          isMainFirstSub: false,
+          expandSub: true
+        }
+      ],
+      picConfig: []
+    }
+  ],
+  basicPaperInfo: {
+    thirdRelateId: 1,
+    thirdRelateName: "考试1",
+    courseName: "语文",
+    courseCode: "yw001",
+    paperNumber: 112345667,
+    paperType: "AB",
+    paperTypes: ["A", "B"],
+    status: "FINISH"
+  }
+};

+ 44 - 27
src/modules/stmms/views/UploadStructure.vue

@@ -1,13 +1,27 @@
 <template>
   <div class="upload-structure">
-    <div
-      v-if="checkPrivilege('button', 'select')"
-      class="part-box part-box-filter part-box-flex"
-    >
-      <div></div>
-      <div class="part-box-action">
-        <el-button type="primary" @click="toPage(1)">查询</el-button>
-      </div>
+    <div class="part-box part-box-filter part-box-flex">
+      <el-form ref="FilterForm" label-position="left" inline>
+        <template v-if="!checkPrivilege('condition', 'condition')">
+          <el-form-item label="学期:">
+            <semester-select v-model="filter.semesterId"></semester-select>
+          </el-form-item>
+          <el-form-item label="考试:">
+            <exam-select
+              v-model="filter.examId"
+              :semester-id="filter.semesterId"
+            ></exam-select>
+          </el-form-item>
+        </template>
+        <el-form-item label-width="0px">
+          <el-button
+            v-if="!checkPrivilege('button', 'select')"
+            type="primary"
+            @click="toPage(1)"
+            >查询</el-button
+          >
+        </el-form-item>
+      </el-form>
     </div>
     <div class="part-box part-box-pad">
       <el-table ref="TableList" :data="dataList">
@@ -110,7 +124,11 @@
       ref="PreviewPaperStructureDialog"
       :instance="curTask"
     />
-    <ModifyMarkParams ref="ModifyMarkParams" :instance="curTask" />
+    <ModifyMarkParams
+      ref="ModifyMarkParams"
+      :instance="curTask"
+      @modified="getList"
+    />
   </div>
 </template>
 
@@ -118,7 +136,7 @@
 import { examStructureListPage } from "../api";
 import UploadPaperAnswerDialog from "../components/UploadPaperAnswerDialog";
 import PreviewPaperStructureDialog from "../components/PreviewPaperStructureDialog";
-import ModifyMarkParams from "../components/ModifyMarkParams";
+import ModifyMarkParams from "../components/markParam/ModifyMarkParams";
 
 export default {
   name: "upload-structure",
@@ -129,7 +147,10 @@ export default {
   },
   data() {
     return {
-      filter: {},
+      filter: {
+        semesterId: "",
+        examId: ""
+      },
       current: 1,
       size: this.GLOBAL.pageSize,
       total: 0,
@@ -138,17 +159,17 @@ export default {
     };
   },
   mounted() {
-    // this.toPage(1);
-    this.dataList.push({
-      thirdRelateId: 1,
-      thirdRelateName: "考试1",
-      courseName: "语文",
-      courseCode: "yw001",
-      paperNumber: 112345667,
-      paperType: "AB",
-      paperTypes: ["A", "B"],
-      status: "FINISH"
-    });
+    this.toPage(1);
+    // this.dataList.push({
+    //   thirdRelateId: 1,
+    //   thirdRelateName: "考试1",
+    //   courseName: "语文",
+    //   courseCode: "yw001",
+    //   paperNumber: 112345667,
+    //   paperType: "AB",
+    //   paperTypes: ["A", "B"],
+    //   status: "FINISH"
+    // });
   },
   methods: {
     async getList() {
@@ -159,11 +180,7 @@ export default {
         pageSize: this.size
       };
       const data = await examStructureListPage(datas);
-      this.dataList = data.records.map(item => {
-        item.paperTypes = item.paperType ? item.paperType.split(",") : [];
-        item.paperAnswer = JSON.parse(item.paperAnswer);
-        return item;
-      });
+      this.dataList = data.records;
       this.total = data.total;
     },
     toPage(page) {