Răsfoiți Sursa

feat: 打回页面

zhangjie 10 luni în urmă
părinte
comite
cf17d923ff

+ 8 - 0
src/api/markPage.ts

@@ -187,3 +187,11 @@ export async function doUnselectiveType() {
     params: getMarkInfo(),
   });
 }
+
+/** 打回评卷任务 */
+export async function doRejectTask(params: {
+  id: string;
+  rejectReason: string;
+}) {
+  return httpApp.post<boolean>("/api/admin/mark/task/reject", params);
+}

+ 105 - 0
src/features/reject/Reject.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="mark">
+    <div class="mark-header">
+      <div v-if="store.currentTask" class="mark-header-part">
+        <div class="header-noun">
+          <span>课程名称:</span>
+          <span>
+            {{ store.currentTask.subject.name }}({{
+              store.currentTask.subject.code
+            }})</span
+          >
+        </div>
+        <div class="header-noun">
+          <span>试卷编号:</span>
+          <span>{{ store.currentTask.paperNumber }}</span>
+        </div>
+        <div class="header-noun">
+          <span>姓名:</span>
+          <span>{{ store.currentTask.studentName }}</span>
+        </div>
+        <div class="header-noun">
+          <span>学号:</span>
+          <span>{{ store.currentTask.studentCode }}</span>
+        </div>
+      </div>
+      <div class="mark-header-part">
+        <div class="header-text-btn header-logout" @click="logout">
+          <img class="header-icon" src="@/assets/icons/icon-return.svg" />返回
+        </div>
+      </div>
+    </div>
+
+    <mark-tool :actions="['minimap', 'sizeScale', 'imgScale']" />
+
+    <div class="mark-main">
+      <mark-body origImageUrls="sheetUrls" onlyTrack @error="renderError" />
+      <reject-board v-if="store.currentTask" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted } from "vue";
+import { store } from "@/store/store";
+import MarkTool from "@/features/mark/MarkTool.vue";
+import MarkBody from "../student/studentInspect/MarkBody.vue";
+import RejectBoard from "./RejectBoard.vue";
+import { message } from "ant-design-vue";
+
+import { getHistoryTask } from "@/api/markPage";
+import vls from "@/utils/storage";
+import { doLogout } from "@/api/markPage";
+
+// groupNumber,studentId,paperNumber,examId,secretNumber,studentCode, studentName,courseCode,courseName
+const rejectParam = vls.get("reject", {});
+
+function logout() {
+  doLogout();
+}
+
+async function updateTask() {
+  const mkey = "fetch_task_key";
+  void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
+
+  const { paperNumber, groupNumber, examId, secretNumber } = rejectParam;
+  const datas = {
+    secretNumber,
+    examId,
+    paperNumber,
+    groupNumber,
+    order: "marker_time",
+    sort: "DESC",
+    pageNumber: 1,
+    pageSize: 20,
+  };
+  const res = await getHistoryTask(datas);
+  if (!res?.data) {
+    store.message = "无数据!";
+    return;
+  }
+
+  const rawTask = res.data.records[0];
+  rawTask.subject = {
+    code: rejectParam.courseCode,
+    name: rejectParam.courseName,
+  };
+  rawTask.paperNumber = paperNumber;
+  rawTask.studentCode = rejectParam.studentCode;
+  rawTask.studentName = rejectParam.studentName;
+  store.currentTask = rawTask;
+}
+
+async function fetchTask() {
+  await updateTask();
+}
+
+onMounted(async () => {
+  await fetchTask();
+});
+
+const renderError = () => {
+  store.currentTask = undefined;
+  store.message = "加载失败,请重新加载。";
+};
+</script>

+ 169 - 0
src/features/reject/RejectBoard.vue

@@ -0,0 +1,169 @@
+<template>
+  <div class="reject-board">
+    <div class="board-header">
+      <div class="board-header-info">
+        <img src="@/assets/icons/icon-star.svg" />
+        <p>评卷总分</p>
+      </div>
+      <div class="board-header-score">
+        <span>{{ store.currentTask?.markerScore }}</span>
+      </div>
+    </div>
+    <div class="board-body">
+      <div
+        v-for="item in questions"
+        :key="item.mainNumber"
+        class="board-question"
+      >
+        <div class="board-question-head"></div>
+        <a-table
+          :dataSource="item.subQuestions"
+          :columns="columns"
+          :showHeader="false"
+        />
+      </div>
+    </div>
+    <div class="board-footer">
+      <qm-button class="board-goback" :clickTimeout="300" @click="openModal">
+        打回
+      </qm-button>
+    </div>
+
+    <!-- 打回原因 -->
+    <qm-dialog
+      v-if="rejectModalVisible"
+      top="10vh"
+      width="500px"
+      height="400px"
+      title="打回原因"
+      @close="closeModal"
+    >
+      <a-form
+        ref="formRef"
+        :model="formState"
+        :rules="rules"
+        @finish="toReject"
+      >
+        <a-form-item name="rejectReason">
+          <a-textarea
+            v-model:value="formState.rejectReason"
+            showCount
+            :maxlength="100"
+          />
+        </a-form-item>
+
+        <a-form-item>
+          <a-button type="primary" htmlType="submit">确定</a-button>
+          <a-button style="margin-left: 10px" @click="closeModal"
+            >取消</a-button
+          >
+        </a-form-item>
+      </a-form>
+    </qm-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { Rule } from "ant-design-vue/es/form";
+import { message, type FormInstance } from "ant-design-vue";
+import { onMounted, ref, reactive } from "vue";
+
+import { store } from "@/store/store";
+import { doRejectTask } from "@/api/markPage";
+
+interface SubQuestion {
+  subNumber: string;
+  score: number;
+}
+
+interface QuestionGroupItem {
+  mainNumber: number;
+  subQuestions: SubQuestion[];
+}
+
+const questions = ref<QuestionGroupItem[]>([]);
+
+const columns = [
+  {
+    title: "小题",
+    dataIndex: "subNumber",
+    key: "subNumber",
+  },
+  {
+    title: "得分",
+    dataIndex: "score",
+    key: "score",
+    width: 60,
+  },
+];
+
+function parseQuestions() {
+  if (!store.currentTask?.questionList) return;
+
+  const questionList = [];
+
+  store.currentTask.questionList.forEach((q) => {
+    if (!questionList[q.mainNumber]) {
+      questionList[q.mainNumber] = {
+        mainNumber: q.mainNumber,
+        subQuestions: [],
+      };
+    }
+
+    questionList[q.mainNumber].push({
+      subNumber: q.subNumber,
+      score: q.score,
+    });
+  });
+
+  questions.value = questionList.filter((item) => !item);
+}
+
+interface FormState {
+  rejectReason: string;
+}
+const formRef = ref<FormInstance>();
+const formState = reactive<FormState>({
+  rejectReason: "",
+});
+const rules: Record<string, Rule[]> = {
+  rejectReason: [{ required: true, message: "请输入打回原因!" }],
+};
+
+const rejectModalVisible = ref(false);
+function closeModal() {
+  rejectModalVisible.value = false;
+}
+function openModal() {
+  formState.rejectReason = "";
+  rejectModalVisible.value = true;
+}
+
+async function toReject(values: FormState) {
+  const res = await doRejectTask({
+    id: store.currentTask.taskId,
+    rejectReason: values.rejectReason,
+  }).catch(() => false);
+  const mkey = "reject_task_key";
+
+  if (!res) return;
+  if (!res.data.success) {
+    console.log(res.data.message);
+    void message.error({ content: res.data.message, key: mkey, duration: 5 });
+    return;
+  }
+
+  void message.success({
+    content: "操作成功,页面将在3秒钟之后关闭",
+    key: mkey,
+    duration: 2,
+  });
+  setTimeout(() => {
+    window.close();
+  }, 3000);
+}
+
+onMounted(() => {
+  parseQuestions();
+});
+</script>

+ 80 - 77
src/features/student/studentInspect/MarkBody.vue

@@ -24,89 +24,91 @@
             :dx="0"
             :dy="0"
           />
-          <!-- 客观题答案标记 -->
-          <template v-if="item.answerTags">
-            <div
-              v-for="(tag, tindex) in item.answerTags"
-              :key="`tag-${tindex}`"
-              :style="tag.style"
-            >
-              {{ tag.answer }}
-            </div>
-          </template>
-          <!-- 试题评分明细 -->
-          <template v-if="item.markDetail">
-            <div
-              v-for="(minfo, mindex) in item.markDetail"
-              :key="`mark-${mindex}`"
-              :style="minfo.style"
-              class="mark-info"
-            >
-              <div v-if="minfo.isFillQuestion">
-                <div
-                  v-for="user in minfo.users"
-                  :key="user.userId"
-                  :style="{ color: user.color }"
-                >
-                  <p>{{ user.prename }}:{{ user.userName }},评分:</p>
-                  <p>
-                    {{
-                      user.scores
-                        .map((s) => `${s.subNumber}:${s.score}分`)
-                        .join(",")
+          <template v-if="!onlyTrack">
+            <!-- 客观题答案标记 -->
+            <template v-if="item.answerTags">
+              <div
+                v-for="(tag, tindex) in item.answerTags"
+                :key="`tag-${tindex}`"
+                :style="tag.style"
+              >
+                {{ tag.answer }}
+              </div>
+            </template>
+            <!-- 试题评分明细 -->
+            <template v-if="item.markDetail">
+              <div
+                v-for="(minfo, mindex) in item.markDetail"
+                :key="`mark-${mindex}`"
+                :style="minfo.style"
+                class="mark-info"
+              >
+                <div v-if="minfo.isFillQuestion">
+                  <div
+                    v-for="user in minfo.users"
+                    :key="user.userId"
+                    :style="{ color: user.color }"
+                  >
+                    <p>{{ user.prename }}:{{ user.userName }},评分:</p>
+                    <p>
+                      {{
+                        user.scores
+                          .map((s) => `${s.subNumber}:${s.score}分`)
+                          .join(",")
+                      }}
+                    </p>
+                  </div>
+                </div>
+                <div v-else>
+                  <p
+                    v-for="user in minfo.users"
+                    :key="user.userId"
+                    :style="{ color: user.color }"
+                  >
+                    {{ user.prename }}:{{ user.userName }},评分:{{
+                      user.score
                     }}
                   </p>
                 </div>
+                <h3>得分:{{ minfo.score }},满分:{{ minfo.maxScore }}</h3>
               </div>
-              <div v-else>
-                <p
-                  v-for="user in minfo.users"
-                  :key="user.userId"
-                  :style="{ color: user.color }"
-                >
-                  {{ user.prename }}:{{ user.userName }},评分:{{
-                    user.score
-                  }}
-                </p>
+            </template>
+            <!-- 客观题 -->
+            <template v-if="item.objectiveAnswerTags">
+              <div
+                v-for="tag in item.objectiveAnswerTags"
+                :key="tag.id"
+                class="mark-objective"
+                :style="tag.style"
+              >
+                得分:{{ tag.score }},满分:{{ tag.totalScore }}
               </div>
-              <h3>得分:{{ minfo.score }},满分:{{ minfo.maxScore }}</h3>
-            </div>
-          </template>
-          <!-- 客观题 -->
-          <template v-if="item.objectiveAnswerTags">
-            <div
-              v-for="tag in item.objectiveAnswerTags"
-              :key="tag.id"
-              class="mark-objective"
-              :style="tag.style"
-            >
-              得分:{{ tag.score }},满分:{{ tag.totalScore }}
-            </div>
-          </template>
-          <!-- 模式4的summary -->
-          <template v-if="item.summarys && item.summarys.length">
-            <div class="summary-detail">
-              <table>
-                <tr>
-                  <th>主观题号</th>
-                  <th>分数</th>
-                  <th>评卷员</th>
-                </tr>
-                <tr v-for="(sinfo, sindex) in item.summarys" :key="sindex">
-                  <td>{{ sinfo.mainNumber }}-{{ sinfo.subNumber }}</td>
-                  <td>{{ sinfo.score }}</td>
-                  <td>{{ sinfo.markerName }}</td>
-                </tr>
-              </table>
+            </template>
+            <!-- 模式4的summary -->
+            <template v-if="item.summarys && item.summarys.length">
+              <div class="summary-detail">
+                <table>
+                  <tr>
+                    <th>主观题号</th>
+                    <th>分数</th>
+                    <th>评卷员</th>
+                  </tr>
+                  <tr v-for="(sinfo, sindex) in item.summarys" :key="sindex">
+                    <td>{{ sinfo.mainNumber }}-{{ sinfo.subNumber }}</td>
+                    <td>{{ sinfo.score }}</td>
+                    <td>{{ sinfo.markerName }}</td>
+                  </tr>
+                </table>
+              </div>
+            </template>
+
+            <!-- 总分 -->
+            <div class="mark-total">
+              总分:{{ totalScore }},主观题得分:{{
+                subjectiveScore
+              }},客观题得分:{{ objectiveScore }}
             </div>
           </template>
-
-          <!-- 总分 -->
-          <div class="mark-total">
-            总分:{{ totalScore }},主观题得分:{{
-              subjectiveScore
-            }},客观题得分:{{ objectiveScore }}
-          </div>
           <hr class="image-seperator" />
         </div>
       </div>
@@ -153,8 +155,9 @@ interface SliceImage {
   summarys?: SummaryItem[];
 }
 
-const { origImageUrls = "sliceUrls" } = defineProps<{
+const { origImageUrls = "sliceUrls", onlyTrack = false } = defineProps<{
   origImageUrls?: "sheetUrls" | "sliceUrls";
+  onlyTrack: boolean;
 }>();
 const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
 

+ 0 - 2
src/features/student/studentTrack/StudentTrack.vue

@@ -35,13 +35,11 @@
       <mark-body @error="renderError" />
     </div>
   </div>
-  <MinimapModal />
 </template>
 
 <script setup lang="ts">
 import { onMounted } from "vue";
 import { store } from "@/store/store";
-import MinimapModal from "@/features/mark/MinimapModal.vue";
 import MarkTool from "@/features/mark/MarkTool.vue";
 import MarkBody from "../studentInspect/MarkBody.vue";
 import { message } from "ant-design-vue";

+ 6 - 0
src/router/index.ts

@@ -30,6 +30,12 @@ const routes = [
     name: "Arbitrate",
     component: () => import("@/features/arbitrate/Arbitrate.vue"),
   },
+  {
+    // 打回
+    path: "/reject",
+    name: "Reject",
+    component: () => import("@/features/reject/Reject.vue"),
+  },
   // old page
   {
     // 整卷批量复核

+ 66 - 0
src/styles/page.less

@@ -1269,6 +1269,72 @@
   }
 }
 
+// reject-board
+.reject-board {
+  width: 360px;
+  padding: 16px;
+  overflow-y: auto;
+  background-color: #fff;
+
+  display: flex;
+  flex-direction: column;
+  border-left: 1px solid #e5e5e5;
+  position: relative;
+  .flex-static;
+
+  .board-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    background-color: #165dff;
+    border-radius: 4px;
+    .flex-static;
+
+    &-info {
+      width: 80px;
+      text-align: center;
+
+      img {
+        display: inline-block;
+        width: 16px;
+        height: 16px;
+        margin-bottom: 5px;
+      }
+    }
+
+    &-score {
+      width: 80px;
+      text-align: center;
+      font-size: 36px;
+    }
+  }
+
+  .board-footer {
+    margin-top: 16px;
+    height: 36px;
+    .flex-static;
+  }
+
+  .board-body {
+    flex-grow: 2;
+    overflow-y: auto;
+    overflow-x: hidden;
+
+    .board-question {
+      background-color: #fff;
+      margin-bottom: 16px;
+
+      &-head {
+        height: 32px;
+        padding: 10px 8px;
+        font-weight: 600;
+        border-bottom: 1px solid #f0f0f0;
+      }
+    }
+  }
+}
+
 // common
 .mark-tooltip {
   padding-top: 0px !important;