Bläddra i källkod

多媒体阅卷

Michael Wang 4 år sedan
förälder
incheckning
4f2f192ae1

+ 7 - 0
src/api/jsonMark.ts

@@ -0,0 +1,7 @@
+import { httpApp } from "@/plugins/axiosApp";
+import { Question } from "@/types";
+
+/** 清理复核任务 */
+export async function getJSON(url: string) {
+  return httpApp.get(url, { withCredentials: false });
+}

+ 1 - 1
src/features/arbitrate/Arbitrate.vue

@@ -108,7 +108,7 @@ async function updateTask() {
 
   if (res.data.libraryId) {
     let rawTask = res.data as Task;
-    rawTask.sliceUrls = rawTask.sliceUrls.map(
+    rawTask.sliceUrls = rawTask.sliceUrls?.map(
       (s) => store.setting.fileServer + s
     );
     rawTask.sheetUrls = rawTask.sheetUrls?.map(

+ 1 - 1
src/features/library/inspect/LibraryInspect.vue

@@ -70,7 +70,7 @@ async function updateTask() {
 
   if (res.data.libraryId) {
     let rawTask = res.data as Task;
-    rawTask.sliceUrls = rawTask.sliceUrls.map(
+    rawTask.sliceUrls = rawTask.sliceUrls?.map(
       (s) => store.setting.fileServer + s
     );
     rawTask.sheetUrls = rawTask.sheetUrls?.map(

+ 11 - 2
src/features/mark/CommonMarkBody.vue

@@ -6,7 +6,10 @@
     <div v-if="!store.currentTask" class="tw-text-center">
       {{ store.message }}
     </div>
-    <div v-else :style="{ width: answerPaperScale }">
+    <div
+      v-else-if="store.setting.examType === 'SCAN_IMAGE'"
+      :style="{ width: answerPaperScale }"
+    >
       <div
         style="
           top: 0px;
@@ -40,6 +43,10 @@
         <hr class="image-seperator" />
       </div>
     </div>
+    <div v-else-if="store.setting.examType === 'MULTI_MEDIA'">
+      <MultiMediaMarkBody />
+    </div>
+    <div v-else>impossible</div>
   </div>
   <slot name="slot-cursor" />
 </template>
@@ -56,7 +63,7 @@ import {
   watch,
   watchEffect,
 } from "vue";
-import { getMarkStatus } from "./store";
+import { getMarkStatus, isScanImage } from "./store";
 import MarkDrawTrack from "./MarkDrawTrack.vue";
 import type {
   MarkResult,
@@ -72,6 +79,7 @@ import {
   loadImage,
 } from "@/utils/utils";
 import { dragImage } from "./use/draggable";
+import MultiMediaMarkBody from "./MultiMediaMarkBody.vue";
 
 const props =
   defineProps<{
@@ -351,6 +359,7 @@ async function processSplitConfig() {
 let renderLock = false;
 const renderPaperAndMark = async () => {
   if (!store) return;
+  if (!isScanImage()) return;
   if (renderLock) {
     console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
     await new Promise((res) => setTimeout(res, 1000));

+ 52 - 3
src/features/mark/Mark.vue

@@ -43,6 +43,7 @@ import {
 } from "@/api/markPage";
 import {
   findCurrentTaskMarkResult,
+  isScanImage,
   removeCurrentMarkResult,
   removeOldPreviousMarkResult,
   store,
@@ -53,7 +54,7 @@ import { useTimers } from "@/setups/useTimers";
 import MarkHistory from "./MarkHistory.vue";
 import MarkBoardTrack from "./MarkBoardTrack.vue";
 import { ModeEnum } from "@/types";
-import type { Setting, Task } from "@/types";
+import type { Setting, Task, RichTextQuestion } from "@/types";
 import MarkBoardKeyBoard from "./MarkBoardKeyBoard.vue";
 import MarkBoardMouse from "./MarkBoardMouse.vue";
 import { isEmpty, isNumber } from "lodash";
@@ -65,6 +66,7 @@ import AllPaperModal from "./AllPaperModal.vue";
 import SheetViewModal from "./SheetViewModal.vue";
 import SpecialTagModal from "./SpecialTagModal.vue";
 import { preDrawImage } from "@/utils/utils";
+import { getJSON } from "@/api/jsonMark";
 
 const { addInterval } = useTimers();
 
@@ -90,8 +92,53 @@ async function updateSetting() {
   if (store.setting.subject?.paperUrl) {
     store.setting.subject.paperUrl =
       store.setting.fileServer + store.setting.subject?.paperUrl;
+
+    const paper = await getPaper();
   }
 }
+
+async function getPaper() {
+  const res = await getJSON(store.setting.subject.paperUrl);
+  store.setting.subject.questions = [];
+  if (res.data.questions) {
+    // 云平台格式
+    const questions = res.data.questions; // TODO: add type
+    for (const q of questions) {
+      const tempQuestion = {
+        unionOrder: q.mainNumber + "-" + q.subNumber,
+        body: q.body,
+        parentBody: q.parentBody,
+        answer: q.answer,
+      } as RichTextQuestion;
+      store.setting.subject.questions.push(tempQuestion);
+    }
+  } else {
+    const details = res.data.details;
+    for (let order1 of details) {
+      for (let order2 of order1) {
+        if (order2.subQuestions) {
+          for (let order3 of order2) {
+            const tempQuestion = {
+              unionOrder:
+                order1.number + "-" + order2.number + "-" + order3.number,
+              body: order3.body,
+              parentBody: order2.body,
+            } as RichTextQuestion;
+            store.setting.subject.questions.push(tempQuestion);
+          }
+        } else {
+          const tempQuestion = {
+            unionOrder: order1.number + "-" + order2.number,
+            body: order2.body,
+            parentBody: null,
+          } as RichTextQuestion;
+          store.setting.subject.questions.push(tempQuestion);
+        }
+      }
+    }
+  }
+}
+
 async function updateStatus() {
   const res = await getStatus();
   if (res.data.valid) store.status = res.data;
@@ -106,7 +153,7 @@ async function updateTask() {
   const res = await getTask();
   if (res.data.libraryId) {
     let rawTask = res.data as Task;
-    rawTask.sliceUrls = rawTask.sliceUrls.map(
+    rawTask.sliceUrls = rawTask.sliceUrls?.map(
       (s) => store.setting.fileServer + s
     );
     rawTask.sheetUrls = rawTask.sheetUrls?.map(
@@ -139,7 +186,9 @@ async function updateTask() {
     // }
     try {
       preDrawing = true;
-      await preDrawImage(res.data);
+      if (isScanImage()) {
+        await preDrawImage(res.data);
+      }
     } finally {
       preDrawing = false;
     }

+ 13 - 1
src/features/mark/MarkBoardKeyBoard.vue

@@ -66,7 +66,12 @@
         :key="index"
       >
         <div
-          @click="chooseQuestion(question)"
+          @click="
+            () => {
+              chooseQuestion(question);
+              scrollToQuestion(question);
+            }
+          "
           class="question tw-rounded tw-p-1 tw-mb-2 tw-cursor-pointer"
           :class="isCurrentQuestion(question) && 'current-question'"
         >
@@ -283,6 +288,13 @@ const handleMouseOutWithBoard = () => {
   }
 };
 
+const scrollToQuestion = (question: Question) => {
+  const node = document.querySelector(
+    `#q-${question.mainNumber}-${question.subNumber}`
+  );
+  node && node.scrollIntoView({ behavior: "smooth" });
+};
+
 function submit() {
   const errors: any = [];
   store.currentTask?.questionList.forEach((question, index) => {

+ 77 - 0
src/features/mark/MultiMediaMarkBody.vue

@@ -0,0 +1,77 @@
+<template>
+  <div>
+    <div ref="rendered" class="rich-text-question-container"></div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { getJSON } from "@/api/jsonMark";
+import { store } from "./store";
+import { ref, watch } from "vue";
+import { renderRichText } from "@/utils/renderJSON";
+import type { RichTextJSON } from "@/types";
+
+const rendered = ref(null as unknown as HTMLDivElement);
+async function updateStudentAnswerJSON() {
+  return getJSON(store.currentTask?.jsonUrl as string);
+}
+
+watch(
+  () => store.currentTask,
+  async () => {
+    const res = await updateStudentAnswerJSON();
+    const questions = res.data; // TODO: add type
+    // TODO: 最好变成有结构的v-for渲染
+    for (const q of questions) {
+      const questionBody = store.setting.subject.questions.find(
+        (ques) => ques.unionOrder === `${q.mainNumber}-${q.subNumber}`
+      );
+      // const jsonContent = q.answer;
+      function appendRichText(rt: RichTextJSON) {
+        const richText = renderRichText(rt);
+        richText && rendered.value.appendChild(richText);
+        return richText;
+      }
+      const questionOrderNode = document.createElement("div");
+      questionOrderNode.innerHTML = `<span id="q-${questionBody?.unionOrder}">题号:${questionBody?.unionOrder}</span>`;
+      rendered.value.appendChild(questionOrderNode);
+
+      const questionBodyNode = document.createElement("div");
+      questionBodyNode.innerHTML = `题干:`;
+      rendered.value.appendChild(questionBodyNode);
+
+      questionBody?.parentBody && appendRichText(questionBody?.parentBody);
+      questionBody?.body && appendRichText(questionBody?.body);
+
+      const studentAnswerNode = document.createElement("div");
+      studentAnswerNode.innerHTML = `考生答案:(字数统计:${
+        renderRichText(q.answer)?.innerHTML?.length ?? 0
+      })`;
+      rendered.value.appendChild(studentAnswerNode);
+
+      q.answer && appendRichText(q.answer);
+
+      const standardAnswerNode = document.createElement("div");
+      standardAnswerNode.innerHTML = `标准答案:`;
+      rendered.value.appendChild(standardAnswerNode);
+
+      questionBody?.answer && appendRichText(questionBody.answer);
+
+      const questionSeperatorNode = document.createElement("hr");
+      questionSeperatorNode.style.margin = "20px";
+      rendered.value.appendChild(questionSeperatorNode);
+      // q.answer && appendRichText(q.answer);
+    }
+  },
+  { immediate: true }
+);
+</script>
+
+<style scoped>
+.rich-text-question-container {
+  background-color: white;
+}
+hr {
+  margin: 20px;
+}
+</style>

+ 8 - 0
src/features/mark/store.ts

@@ -117,4 +117,12 @@ export function getMarkStatus() {
   return "";
 }
 
+/**
+ * 是否是扫描阅卷
+ * @returns boolean
+ */
+export function isScanImage() {
+  return store.setting.examType === "SCAN_IMAGE";
+}
+
 export const store = reactive(obj);

+ 1 - 1
src/features/student/inspect/Inspect.vue

@@ -93,7 +93,7 @@ async function updateTask() {
 
   if (res.data.studentId) {
     let rawTask = res.data as Task;
-    rawTask.sliceUrls = rawTask.sliceUrls.map(
+    rawTask.sliceUrls = rawTask.sliceUrls?.map(
       (s) => store.setting.fileServer + s
     );
     rawTask.sheetUrls = rawTask.sheetUrls?.map(

+ 1 - 1
src/plugins/axiosApp.ts

@@ -97,7 +97,7 @@ _axiosApp.interceptors.response.use(
   }
 );
 
-_axiosApp.get = cachingGet(_axiosApp, cacheGetUrls);
+// _axiosApp.get = cachingGet(_axiosApp, cacheGetUrls);
 loadProgressBar(null, _axiosApp);
 
 export const httpApp = _axiosApp;

+ 26 - 1
src/types/index.ts

@@ -43,6 +43,7 @@ export interface Setting {
     code: string;
     answerUrl: string;
     paperUrl: string;
+    questions: Array<RichTextQuestion>;
   };
   forceSpecialTag: boolean; //强制标记是否开启 forceTag
   uiSetting: UISetting;
@@ -182,7 +183,7 @@ export interface InspectStore {
   setting: {
     fileServer: string;
     userName: string;
-    subject: { name: string; code: string };
+    subject: { name: string; code: string; paperUrl: string };
     uiSetting: {
       "answer.paper.scale": number;
       "score.board.collapse": boolean;
@@ -223,3 +224,27 @@ export type MarkHistoryOrderBy =
 export type MarkHistorySortField = "ASC" | "DESC" | undefined;
 
 export type UnionStore = MarkStore | InspectStore;
+
+export interface RichTextQuestion {
+  unionOrder: string; // 题目的综合题号 1-2-4
+  body: RichTextJSON;
+  parentBody: RichTextJSON | null;
+  answer: RichTextJSON | null;
+}
+export interface RichTextJSON {
+  sections: RichTextSectionJSON[];
+}
+export interface RichTextSectionJSON {
+  blocks: RichTextBlockJSON[];
+}
+export interface RichTextBlockJSON {
+  type: "text" | "image" | "audio" | "cloze";
+  value: string;
+  param: {
+    underline: boolean;
+    bold: boolean;
+    italic: boolean;
+    width: string;
+    height: string;
+  } | null;
+}

+ 122 - 0
src/utils/renderJSON.ts

@@ -0,0 +1,122 @@
+// const _text_styles_ = ["bold", "underline", "italic", "sup", "sub"];
+
+import { RichTextBlockJSON, RichTextJSON, RichTextSectionJSON } from "@/types";
+
+let _container = document.createElement("div");
+/**
+ * 将富文本 JSON 渲染到指定的元素中
+ *
+ * @param {RichTextJSON} body
+ * @param {HTMLDivElement} container
+ */
+export function renderRichText(body: RichTextJSON, container?: HTMLDivElement) {
+  _container = container || document.createElement("div");
+  let sections = body?.sections || [];
+  let nodes = [] as Array<Node>;
+  sections.forEach((section) => {
+    nodes.push(renderSection(section));
+  });
+  if (_container != undefined) {
+    // container.classList.add("rich-text");
+    while (_container.hasChildNodes()) {
+      _container.removeChild(_container.lastChild as Node);
+    }
+    nodes.forEach((node) => {
+      _container.appendChild(node);
+    });
+  }
+
+  return _container;
+}
+
+/**
+ * @param {RichTextSectionJSON} section
+ * @returns {HTMLDivElement} 返回根据 section 渲染好的 HTMLDivElement
+ */
+function renderSection(section: RichTextSectionJSON) {
+  let blocks = section.blocks || [];
+  let inline = blocks.length > 1;
+  let node = document.createElement("div");
+  // node.style = "display: flex;";
+  blocks.forEach((block) => {
+    node.appendChild(renderBlock(block, inline));
+  });
+  return node;
+}
+
+/**
+ * @param {RichTextBlockJSON} block
+ * @param {Boolean} inline 图片是否以 inline 的样式展示
+ * @returns {HTMLElement} 返回根据 block 渲染好的 HTMLElement
+ */
+function renderBlock(block: RichTextBlockJSON, inline: boolean) {
+  // let node = document.createElement('span')
+  // let classList = node.classList
+  let node;
+  if (block.type === "text") {
+    // classList.add('text')
+    // if (block.param != undefined) {
+    //     _text_styles_.forEach(style => {
+    //         if (block.param[style] === true) {
+    //             classList.add(style)
+    //         }
+    //     })
+    // }
+    if (
+      block.param &&
+      (block.param.underline || block.param.bold || block.param.italic)
+    ) {
+      let uNode: Node | null = null,
+        bNode: Node | null = null,
+        iNode: Node | null = null;
+      if (block.param.underline) {
+        uNode = document.createElement("u");
+      }
+      if (block.param.bold) {
+        bNode = document.createElement("b");
+      }
+      if (block.param.italic) {
+        iNode = document.createElement("i");
+      }
+      // 将不为空的元素依次append
+      node = ([uNode, bNode, iNode] as Array<Node>)
+        .filter((v) => v)
+        .reduceRight((p, c) => {
+          c.appendChild(p);
+          return c;
+        });
+
+      let childNode = node;
+
+      for (let i = 0; i < 3; i++) {
+        if (childNode && childNode.hasChildNodes()) {
+          childNode = childNode.childNodes[0];
+        }
+      }
+
+      childNode.textContent = block.value;
+    } else {
+      node = document.createTextNode(block.value);
+    }
+  } else if (block.type === "image") {
+    node = document.createElement("img");
+    if (inline === true) {
+      node.classList.add("inline");
+    }
+
+    node.src = block.value;
+
+    // param
+    if (block.param) {
+      node.style.width = block.param.width;
+      node.style.height = block.param.height;
+    }
+  } else if (block.type === "audio") {
+    node = document.createElement("audio");
+    node.className = "audio";
+    node.src = block.value;
+    node.controls = true;
+  }
+
+  return node as Node;
+}