Explorar el Código

feat: 云阅卷1.3.8

1. 复核打回增加确认弹窗
2. 轨迹闪烁停止增加3s延迟
3. 多媒体阅卷支持新题型
chenhao hace 2 años
padre
commit
7fac2c94fc

+ 2 - 0
.gitignore

@@ -13,6 +13,8 @@ npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
 pnpm-debug.log*
+metadata.md
+temp.json
 
 # Editor directories and files
 .idea

+ 3 - 1
src/api/inspectPage.ts

@@ -130,7 +130,8 @@ export async function saveInspectedTask(studentId: string) {
 /** 复核任务打回问题 */
 export async function rejectInspectedTask(
   studentId: string,
-  questionList: Array<Question>
+  questionList: Array<Question>,
+  reason: string
 ) {
   questionList = JSON.parse(
     JSON.stringify(questionList, (key, value) =>
@@ -141,5 +142,6 @@ export async function rejectInspectedTask(
   return httpApp.post<CommonResponse>("/admin/exam/inspected/rejected", {
     studentId,
     questionList,
+    reason
   });
 }

+ 37 - 11
src/api/jsonMark.ts

@@ -5,6 +5,7 @@ import type {
   OExamPaperJSON,
   ECSPaperJSON,
 } from "@/types";
+// import TempJson from './temp.json'
 
 /** 获取学生答案JSON */
 export async function getStudentAnswerJSON(url: string) {
@@ -40,17 +41,42 @@ export async function getPaper(store: MarkStore) {
     for (const order1 of details) {
       for (const order2 of order1.questions) {
         if (order2.subQuestions) {
-          for (const order3 of order2.subQuestions) {
-            const tempQuestion = {
-              unionOrder:
-                order1.number + "-" + order2.number + "-" + order3.number,
-              body: order3.body,
-              parentBody: order2.body,
-              answer: order3.answer,
-              objective: order3.objective,
-              options: order3.options,
-            };
-            store.setting.subject.questions.push(tempQuestion);
+          if (order2.structType === 8) {
+            store.setting.subject.questions.push(
+              ...[order2.subQuestions[0], ...order2.subQuestions].map(
+                (order3, i) => {
+                  const unionOrder = `${order1.number}-${order2.number}${
+                    i === 0 ? "" : "-" + order3.number
+                  }`;
+                  const options = i === 0 ? order3.options : null;
+                  const parentBody = i === 0 ? order2.body : null;
+                  const hideAnswer = i === 0
+                  const tempQuestion = {
+                    unionOrder: unionOrder,
+                    body: order3.body,
+                    parentBody,
+                    answer: order3.answer,
+                    objective: order3.objective,
+                    options,
+                    hideAnswer
+                  };
+                  return tempQuestion;
+                }
+              )
+            );
+          } else {
+            for (const order3 of order2.subQuestions) {
+              const tempQuestion = {
+                unionOrder:
+                  order1.number + "-" + order2.number + "-" + order3.number,
+                body: order3.body,
+                parentBody: order2.body,
+                answer: order3.answer,
+                objective: order3.objective,
+                options: order3.options,
+              };
+              store.setting.subject.questions.push(tempQuestion);
+            }
           }
         } else {
           const tempQuestion = {

+ 3 - 1
src/api/libraryInspectPage.ts

@@ -120,7 +120,8 @@ export async function saveInspectedTaskOfLibraryInspect(libraryId: string) {
 /** 复核任务打回问题 */
 export async function rejectInspectedTaskOfLibraryInspect(
   libraryId: string,
-  questionList: Array<Question>
+  questionList: Array<Question>,
+  reason: string
 ) {
   questionList = JSON.parse(
     JSON.stringify(questionList, (key, value) =>
@@ -131,5 +132,6 @@ export async function rejectInspectedTaskOfLibraryInspect(
   return httpApp.post<CommonResponse>("/admin/exam/library/rejected", {
     libraryId,
     questionList,
+    reason,
   });
 }

+ 9 - 2
src/features/library/inspect/LibraryInspect.vue

@@ -178,13 +178,20 @@ const saveTaskToServer = async () => {
   }
 };
 
-const rejectQuestions = async (questions: Array<Question>) => {
+const rejectQuestions = async ({
+  questions,
+  reason = "",
+}: {
+  questions: Array<Question>;
+  reason: string;
+}) => {
   if (!store.currentTask) return;
   const mkey = "reject_task_key";
   void message.loading({ content: "打回评卷任务...", key: mkey });
   const res = await rejectInspectedTaskOfLibraryInspect(
     store.currentTask.libraryId + "",
-    questions
+    questions,
+    reason
   );
   if (res.data.success) {
     store.currentTask = undefined;

+ 12 - 2
src/features/library/inspect/MarkBoardInspect.vue

@@ -100,6 +100,10 @@
       >
     </div>
   </div>
+  <review-return-dialog
+    v-model:visible="reviewReturnVisible"
+    @confirmReturn="onConfirmReturn"
+  />
 </template>
 
 <script setup lang="ts">
@@ -111,10 +115,11 @@ import {
   addFocusTrack,
   removeFocusTrack,
 } from "@/features/mark/use/focusTracks";
+import ReviewReturnDialog from "./ReviewReturnDialog.vue";
 
 const emit = defineEmits(["inspect", "reject"]);
 let checkedQuestions: Question[] = reactive([]);
-
+let reviewReturnVisible = $ref(false);
 watch(
   () => store.currentTask,
   () => {
@@ -182,12 +187,17 @@ function reject() {
     void message.warn({ content: "请先选择试题。" });
     return;
   }
-  emit("reject", checkedQuestions);
+  reviewReturnVisible = true;
+  // emit("reject", checkedQuestions);
 }
 
 function inspect() {
   emit("inspect");
 }
+
+function onConfirmReturn(reason: string) {
+  emit("reject", { questions: checkedQuestions, reason });
+}
 </script>
 
 <style scoped>

+ 114 - 0
src/features/library/inspect/ReviewReturnDialog.vue

@@ -0,0 +1,114 @@
+<template>
+  <a-modal
+    v-bind="$attrs"
+    title="打回"
+    :maskClosable="false"
+    :footer="false"
+    :zIndex="6000"
+  >
+    <div ref="modalContenBox">
+      <a-form
+        autocomplete="off"
+        :model="reasonOption"
+        @keydown.stop=""
+        @keypress.stop=""
+        @finish="onConfirm"
+      >
+        <a-form-item label="打回原因" name="reason" required>
+          <a-select
+            v-model:value="reasonOption.reason"
+            placeholder="选择打回原因"
+            :virtual="false"
+            :getPopupContainer="() => modalContenBox!"
+            :options="reasonOptions"
+          >
+          </a-select>
+        </a-form-item>
+        <a-form-item label="详情描述" name="desc">
+          <a-textarea
+            v-model:value="reasonOption.desc"
+            placeholder="详情描述(限制50字以内)"
+            :maxlength="50"
+          ></a-textarea>
+        </a-form-item>
+        <a-form-item>
+          <a-row justify="center">
+            <a-col>
+              <a-button type="primary" htmlType="submit">打回</a-button>
+            </a-col>
+            <a-col :offset="2">
+              <a-button type="ghost" @click="onCancel">取消</a-button>
+            </a-col>
+          </a-row>
+        </a-form-item>
+      </a-form>
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+import { reactive, useAttrs, watch, ref } from "vue";
+
+interface ReturnInfo {
+  reason?: string;
+  desc: string;
+}
+
+/** select 定位层级较低, 所以在弹窗中使用一个div容器, 将select 组件的 dorpdown 挂载在div中, 避免dropdown被modal挡住 */
+const modalContenBox = ref<HTMLElement>();
+
+const emit = defineEmits(["confirmReturn"]);
+
+/** 打回原因元数据 */
+const reasonOptions = ["给分较高", "给分较低", "判分错误", "其它"].map((v) => ({
+  value: v,
+}));
+
+/** 初始化表单数据 */
+const getInitialFormData = (): ReturnInfo => {
+  return {
+    reason: undefined,
+    desc: "",
+  };
+};
+
+const reasonOption = reactive<ReturnInfo>(getInitialFormData());
+
+watch(
+  () => modalContenBox,
+  () => {
+    console.log(modalContenBox);
+  }
+);
+
+/** 弹窗关闭时 重置form表单数据 */
+const attrs = useAttrs();
+watch(
+  () => attrs.visible,
+  (v) => {
+    if (!v) {
+      Object.assign(reasonOption, getInitialFormData());
+    }
+  }
+);
+
+/** 确定打回 */
+const onConfirm = () => {
+  emit(
+    "confirmReturn",
+    Object.values(reasonOption)
+      .map((s) => s || "")
+      .join(":")
+  );
+  onCancel();
+};
+
+/** 取消打回 */
+const onCancel = (e?: MouseEvent) => {
+  (attrs["onUpdate:visible"] as (e: boolean) => void)?.(false);
+  (attrs.onChange as (e: boolean) => void)?.(false);
+  (attrs.onCancel as (e?: MouseEvent) => void)?.(e);
+};
+</script>
+
+<style scoped></style>

+ 40 - 34
src/features/mark/MultiMediaMarkBody.vue

@@ -23,40 +23,44 @@
             </div>
           </template>
         </div>
-        <div>
-          <template v-if="question.objective">
-            <span class="tw-text-blue-600">考生答案:</span
-            >{{ renderObjective(question.studentAnswer) }}
-          </template>
-          <template v-else>
-            <div class="tw-text-blue-600">
-              考生答案:(字数统计:{{
-                getDomByRichTextJSON(question.studentAnswer)?.innerText
-                  .length ?? 0
-              }})
-            </div>
-            <div
-              v-html="getDomByRichTextJSON(question.studentAnswer)?.innerHTML"
-            />
-          </template>
-        </div>
-        <div>
-          <template v-if="question.objective">
-            <span class="tw-text-blue-600">标准答案:</span
-            >{{ renderObjective(question.standardAnswer) }}
-          </template>
-          <template v-else>
-            <div class="tw-text-blue-600">标准答案:</div>
-            <div
-              v-html="getDomByRichTextJSON(question.standardAnswer)?.innerHTML"
-            />
-          </template>
-        </div>
-        <div v-if="showScore(question)" style="color: blue">
-          得分 / 总分 :{{
-            (question.score ?? "-") + " / " + question.totalScore
-          }}
-        </div>
+        <template v-if="!question.hideAnswer">
+          <div>
+            <template v-if="question.objective">
+              <span class="tw-text-blue-600">考生答案:</span
+              >{{ renderObjective(question.studentAnswer) }}
+            </template>
+            <template v-else>
+              <div class="tw-text-blue-600">
+                考生答案:(字数统计:{{
+                  getDomByRichTextJSON(question.studentAnswer)?.innerText
+                    .length ?? 0
+                }})
+              </div>
+              <div
+                v-html="getDomByRichTextJSON(question.studentAnswer)?.innerHTML"
+              />
+            </template>
+          </div>
+          <div>
+            <template v-if="question.objective">
+              <span class="tw-text-blue-600">标准答案:</span
+              >{{ renderObjective(question.standardAnswer) }}
+            </template>
+            <template v-else>
+              <div class="tw-text-blue-600">标准答案:</div>
+              <div
+                v-html="
+                  getDomByRichTextJSON(question.standardAnswer)?.innerHTML
+                "
+              />
+            </template>
+          </div>
+          <div v-if="showScore(question)" style="color: blue">
+            得分 / 总分 :{{
+              (question.score ?? "-") + " / " + question.totalScore
+            }}
+          </div>
+        </template>
       </div>
       <div style="margin-bottom: 20px"></div>
     </div>
@@ -136,6 +140,7 @@ watch(
           body: questionBody.body,
           options: questionBody.options,
           objective: questionBody.objective,
+          hideAnswer: questionBody.hideAnswer,
           standardAnswer: questionBody.answer,
           studentAnswer: stuAns.answer,
           score: taskQuestion?.score ?? null,
@@ -171,6 +176,7 @@ watch(
           options: questionBody.options,
           objective: questionBody.objective,
           standardAnswer: questionBody.answer,
+          hideAnswer: questionBody.hideAnswer,
           studentAnswer: stuAns.answer,
           score: taskQuestion.score,
           totalScore: taskQuestion.maxScore,

+ 10 - 3
src/features/mark/use/focusTracks.ts

@@ -60,8 +60,15 @@ function _addFocusTrack(
   // console.log(store.focusTracks);
 }
 
-export function removeFocusTrack() {
+let removeTrackTimer: number | null = null;
+
+export const removeFocusTrack = function removeFocusTrack() {
   hovering = false;
   clearTimeout(timeoutId);
-  store.focusTracks.splice(0);
-}
+  removeTrackTimer && clearTimeout(removeTrackTimer);
+  removeTrackTimer = setTimeout(() => {
+    if (!hovering) {
+      store.focusTracks.splice(0);
+    }
+  }, 3000);
+};

+ 12 - 3
src/features/student/studentInspect/MarkBoardInspect.vue

@@ -100,6 +100,10 @@
       >
     </div>
   </div>
+  <review-return-dialog
+    v-model:visible="reviewReturnVisible"
+    @confirmReturn="onConfirmReturn"
+  />
 </template>
 
 <script setup lang="ts">
@@ -111,10 +115,10 @@ import {
   addFocusTrack,
   removeFocusTrack,
 } from "@/features/mark/use/focusTracks";
-
+import ReviewReturnDialog from "@/features/library/inspect/ReviewReturnDialog.vue";
 const emit = defineEmits(["inspect", "reject"]);
 let checkedQuestions: Question[] = reactive([]);
-
+let reviewReturnVisible = $ref(false);
 watch(
   () => store.currentTask,
   () => {
@@ -182,12 +186,17 @@ function reject() {
     void message.warn({ content: "请先选择试题。" });
     return;
   }
-  emit("reject", checkedQuestions);
+  reviewReturnVisible = true;
+  // emit("reject", checkedQuestions);
 }
 
 function inspect() {
   emit("inspect");
 }
+
+function onConfirmReturn(reason: string) {
+  emit("reject", { questions: checkedQuestions, reason });
+}
 </script>
 
 <style scoped>

+ 8 - 2
src/features/student/studentInspect/StudentInspect.vue

@@ -186,10 +186,16 @@ const saveTaskToServer = async () => {
   }
 };
 
-const rejectQuestions = async (questions: Array<Question>) => {
+const rejectQuestions = async ({
+  questions,
+  reason = "",
+}: {
+  questions: Array<Question>;
+  reason: string;
+}) => {
   const mkey = "reject_task_key";
   void message.loading({ content: "打回评卷任务...", key: mkey });
-  const res = await rejectInspectedTask(realStudentId, questions);
+  const res = await rejectInspectedTask(realStudentId, questions, reason);
   if (res.data.success && store.currentTask) {
     store.currentTask = undefined;
     void message.success({ content: "打回成功", key: mkey, duration: 2 });

+ 3 - 0
src/types/index.ts

@@ -398,6 +398,7 @@ export interface RichTextQuestion {
   answer: Array<RichTextJSON> | null;
   objective: boolean | null;
   options: Array<{ number: number; body: RichTextJSON }> | null;
+  hideAnswer?: boolean
 }
 
 export interface QuestionForRender extends Omit<RichTextQuestion, "answer"> {
@@ -456,5 +457,7 @@ interface OExamPaperJSONQuestion {
   objective: boolean | null;
   options: Array<{ number: number; body: RichTextJSON }>;
   subQuestions: OExamPaperJSONQuestion[] | null;
+  /** 1-单选,2-多选,3-判断,4-填空,5-问答,6-套题,7-听力,8-配对题 */
+  structType: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
 }
 //#endregion