瀏覽代碼

新增成绩校验页面

刘洋 1 年之前
父節點
當前提交
ced352d1ef

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "stmms-web",
-  "version": "1.3.11",
+  "version": "1.3.12",
   "private": "true",
   "scripts": {
     "start": "vite --host 0.0.0.0",

+ 9 - 1
src/components/CommonMarkHeader.vue

@@ -22,10 +22,17 @@
     <div
       class="tw-text-white tw-block tw-overflow-ellipsis tw-overflow-hidden tw-whitespace-nowrap header-big-text tw-ml-4"
     >
-      {{
+      <!-- {{
         `${store.setting.subject?.code ?? ""}-${
           store.setting.subject?.name ?? ""
         }`
+      }} -->
+      {{
+        `${
+          store.setting.subject?.code || store.currentTask?.subject?.code || ""
+        }-${
+          store.setting.subject?.name || store.currentTask?.subject?.name || ""
+        }`
       }}
     </div>
 
@@ -39,6 +46,7 @@
         </div>
       </slot>
     </div>
+    <slot name="studentInfo"> </slot>
 
     <div
       v-if="!isSingleStudent"

+ 2 - 2
src/devLoginParams.ts

@@ -59,9 +59,9 @@
 export const LOGIN_CONFIG = {
   isAdmin: true,
   forceChange: true,
-  loginName: "admin2",
+  loginName: "admin-test",
   password: "123456",
-  examId: "37",
+  examId: "1",
   markerId: null,
 };
 

+ 226 - 0
src/features/student/scoreVerify/MarkBoardInspect.vue

@@ -0,0 +1,226 @@
+<template>
+  <div
+    v-if="store.currentTask"
+    class="mark-board-track-container"
+    :class="[store.isScoreBoardCollapsed ? 'hide' : 'show']"
+  >
+    <div class="top-container tw-flex-shrink-0 tw-flex tw-items-center">
+      <div class="tw-flex tw-flex-col tw-flex-1 tw-text-center">
+        <div class="tw-flex tw-justify-center">
+          <img
+            src="../../mark/images/totalscore.png"
+            style="width: 13px; height: 16px"
+          />
+        </div>
+        <div>试卷总分</div>
+      </div>
+      <div class="tw-flex-1" style="font-size: 40px">
+        {{ markerScore > 0 ? markerScore : 0 }}
+      </div>
+      <div
+        class="star"
+        :class="props.tagged ? 'star-yes' : 'star-no'"
+        @click="makeTag(!props.tagged)"
+      ></div>
+    </div>
+
+    <div v-if="groups" class="tw-flex-grow tw-overflow-auto tw-my-5">
+      <template v-for="(groupNumber, index) in groups" :key="index">
+        <div class="tw-mb-4 tw-bg-white tw-p-4 tw-pl-5 tw-pr-3">
+          <div
+            class="tw-flex tw-justify-between tw-place-items-center hover:tw-bg-gray-200"
+            @mouseover="addFocusTrack(groupNumber, undefined, undefined)"
+            @mouseleave="removeFocusTrack"
+          >
+            <span class="secondary-text">分组 {{ groupNumber }}</span>
+          </div>
+          <div v-if="questions">
+            <template v-for="(question, index2) in questions" :key="index2">
+              <div
+                v-if="question.groupNumber === groupNumber"
+                class="question tw-flex tw-place-items-center tw-mb-1 tw-font-bold hover:tw-bg-gray-200"
+                :class="{ uncalculate: question.uncalculate }"
+                @mouseover="
+                  addFocusTrack(
+                    undefined,
+                    question.mainNumber,
+                    question.subNumber
+                  )
+                "
+                @mouseleave="removeFocusTrack"
+              >
+                <a-tooltip placement="left">
+                  <template #title>
+                    <span>未计入总分</span>
+                  </template>
+                  <MinusCircleFilled class="uncalculate-icon" />
+                </a-tooltip>
+                <span class="question-title">
+                  {{ question.title }} {{ question.mainNumber }}-{{
+                    question.subNumber
+                  }}
+                </span>
+                <span class="tw-text-center question-score">
+                  {{ question.score === -1 ? "未选做" : question.score || 0 }}
+                </span>
+              </div>
+            </template>
+          </div>
+        </div>
+      </template>
+    </div>
+
+    <div class="tw-flex tw-flex-shrink-0 tw-justify-center tw-gap-4">
+      <a-button
+        type="primary"
+        class="full-width-btn"
+        :disabled="props.isFirst"
+        @click="fetchTask(false)"
+      >
+        上一个
+      </a-button>
+      <a-button
+        type="primary"
+        class="full-width-btn"
+        :disabled="props.isLast"
+        @click="fetchTask(true)"
+        >下一个</a-button
+      >
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { Question } from "@/types";
+import { reactive, watch } from "vue";
+import { store } from "@/store/store";
+import {
+  addFocusTrack,
+  removeFocusTrack,
+} from "@/features/mark/use/focusTracks";
+import { MinusCircleOutlined, MinusCircleFilled } from "@ant-design/icons-vue";
+
+const emit = defineEmits(["makeTag", "fetchTask"]);
+const props = defineProps<{
+  tagged: boolean;
+  isFirst: boolean;
+  isLast: boolean;
+}>();
+let checkedQuestions: Question[] = reactive([]);
+
+watch(
+  () => store.currentTask,
+  () => {
+    checkedQuestions.splice(0);
+  }
+);
+const groups = $computed(() => {
+  const gs = store.currentTaskEnsured.questionList.map((q) => q.groupNumber);
+  return [...new Set(gs)].sort((a, b) => a - b);
+});
+
+const questions = $computed(() => {
+  const qs = store.currentTaskEnsured.questionList;
+  return qs;
+});
+
+const markerScore = $computed(() => store.currentTaskEnsured.markerScore || 0);
+
+function fetchTask(next: boolean) {
+  emit("fetchTask", next);
+}
+
+function makeTag(isTag: boolean) {
+  emit("makeTag", isTag);
+}
+</script>
+
+<style scoped>
+.mark-board-track-container {
+  display: flex;
+  flex-direction: column;
+  max-width: 290px;
+  min-width: 290px;
+  max-height: calc(100vh - 56px);
+  padding: 20px;
+  z-index: 1001;
+  transition: margin-right 0.5s;
+  color: var(--app-small-header-text-color);
+}
+.mark-board-track-container.show {
+  margin-right: 0;
+}
+.mark-board-track-container.hide {
+  margin-right: -290px;
+}
+
+.top-container {
+  background-color: var(--app-container-bg-color);
+  height: 86px;
+  border-radius: 5px;
+
+  color: white;
+  background-color: var(--app-primary-button-bg-color);
+}
+.question {
+  min-width: 80px;
+  background-color: var(--app-container-bg-color);
+}
+.question-title {
+  flex: 1;
+}
+.question-score {
+  flex-basis: 56px;
+  padding: 0 3px;
+}
+
+.question.uncalculate {
+  position: relative;
+}
+
+.question .uncalculate-icon {
+  display: none;
+  color: red;
+  position: absolute;
+  font-size: 15px;
+  left: -16px;
+  top: 0.3em;
+}
+
+.question.uncalculate .uncalculate-icon {
+  display: block;
+}
+
+.full-width-btn {
+  width: 100%;
+  border-radius: 20px;
+}
+
+.star {
+  margin-top: -30px;
+  margin-right: 20px;
+  width: 30px;
+  height: 30px;
+  cursor: pointer;
+
+  clip-path: polygon(
+    50% 0%,
+    61% 35%,
+    98% 35%,
+    68% 57%,
+    79% 91%,
+    50% 70%,
+    21% 91%,
+    32% 57%,
+    2% 35%,
+    39% 35%
+  );
+}
+
+.star.star-yes {
+  background-color: yellowgreen;
+}
+.star.star-no {
+  background-color: white;
+}
+</style>

+ 55 - 0
src/features/student/scoreVerify/MarkHeader.vue

@@ -0,0 +1,55 @@
+<template>
+  <CommonMarkHeader
+    :isSingleStudent="isSingleStudent"
+    :clearTasks="clearTasks"
+    showScoreBoard
+  >
+    <slot name="taskInfo">
+      <div>
+        <span class="header-small-text">学号</span>
+        <span class="highlight-text">
+          {{ store.currentTask?.studentCode ?? "-" }}
+        </span>
+      </div>
+      <div>
+        <span class="header-small-text">姓名</span>
+        <span class="highlight-text">
+          {{ store.currentTask?.studentName ?? "-" }}
+        </span>
+      </div>
+    </slot>
+    <span>
+      <span class="header-small-text">待复核</span>
+      <span class="highlight-text">{{
+        store.status.totalCount - store.status.markedCount ?? "-"
+      }}</span>
+    </span>
+
+    <template #studentInfo
+      ><div class="highlight-text">
+        考生:
+        {{
+          store.currentTask?.studentCode +
+          " - " +
+          store.currentTask?.studentName
+        }}
+      </div></template
+    >
+  </CommonMarkHeader>
+</template>
+
+<script setup lang="ts">
+import { clearInspectedTask } from "@/api/inspectPage";
+import { store } from "@/store/store";
+import { useRoute } from "vue-router";
+import CommonMarkHeader from "@/components/CommonMarkHeader.vue";
+
+const route = useRoute();
+let isSingleStudent = !!route.query.studentId;
+const { studentId, subjectCode } = route.query as {
+  studentId: string;
+  subjectCode: string;
+};
+
+let clearTasks = clearInspectedTask.bind(null, studentId, subjectCode);
+</script>

+ 141 - 0
src/features/student/scoreVerify/ScoreVerify.vue

@@ -0,0 +1,141 @@
+<template>
+  <div class="my-container">
+    <mark-header />
+    <div class="tw-flex tw-gap-1">
+      <mark-body origImageUrls="sheetUrls" @error="renderError" />
+      <MarkBoardInspect
+        :tagged="isCurrentTagged"
+        :isFirst="isFirst"
+        :isLast="isLast"
+        @makeTag="saveTaskToServer"
+        @fetchTask="fetchTask"
+      />
+    </div>
+  </div>
+  <MinimapModal />
+</template>
+
+<script setup lang="ts">
+import { onMounted } from "vue";
+import {
+  getInspectedSettingOfImportInspect,
+  getSingleInspectedTaskOfImportInspect,
+  saveInspectedTaskOfImportInspect,
+} from "@/api/importInspectPage";
+import { store } from "@/store/store";
+import MarkHeader from "./MarkHeader.vue";
+import MinimapModal from "@/features/mark/MinimapModal.vue";
+import { useRoute } from "vue-router";
+import MarkBody from "../studentInspect/MarkBody.vue";
+import MarkBoardInspect from "./MarkBoardInspect.vue";
+import type { AdminPageSetting } from "@/types";
+import { message } from "ant-design-vue";
+import { addFileServerPrefixToTask } from "@/utils/utils";
+
+const route = useRoute();
+const { studentId } = route.query as {
+  studentId: string;
+};
+
+let studentIds: number[] = $ref([]);
+let tagIds: number[] = $ref([]);
+let currentStudentId = $ref(0);
+
+async function updateSetting() {
+  const settingRes = await getInspectedSettingOfImportInspect(studentId);
+  const { examType, fileServer } = settingRes.data;
+  store.initSetting({ examType, fileServer } as AdminPageSetting);
+  store.status.totalCount = settingRes.data.inspectCount;
+  store.status.markedCount = 0;
+
+  if (!settingRes.data.inspectCount) {
+    store.message = settingRes.data.message;
+  } else {
+    studentIds = settingRes.data.studentIds;
+    tagIds = settingRes.data.tagIds;
+  }
+}
+// 要通过fetchTask调用
+async function updateTask() {
+  if (!currentStudentId) {
+    return;
+  }
+  const mkey = "fetch_task_key";
+  void message.info({ content: "获取任务中...", duration: 1.5, key: mkey });
+  let res = await getSingleInspectedTaskOfImportInspect("" + currentStudentId);
+  void message.success({
+    content: res.data.studentId ? "获取成功" : "无任务",
+    key: mkey,
+  });
+
+  if (res.data.studentId) {
+    let rawTask = res.data;
+    store.currentTask = addFileServerPrefixToTask(rawTask);
+  } else {
+    store.message = res.data.message;
+  }
+}
+
+const isCurrentTagged = $computed(() => tagIds.includes(currentStudentId));
+const isFirst = $computed(() => studentIds.indexOf(currentStudentId) === 0);
+const isLast = $computed(
+  () => studentIds.indexOf(currentStudentId) === studentIds.length - 1
+);
+
+async function fetchTask(next: boolean, init?: boolean) {
+  if (init) {
+    currentStudentId = studentIds[0];
+  } else if (isLast && next) {
+    return; // currentStudentId是最后一个不调用
+  } else if (isFirst && !next) {
+    return; // currentStudentId是第一个不调用
+  } else {
+    currentStudentId =
+      studentIds[studentIds.indexOf(currentStudentId) + (next ? 1 : -1)];
+  }
+  if (!currentStudentId) return; // 无currentStudentId不调用
+  store.status.markedCount = studentIds.indexOf(currentStudentId) + 1;
+  await updateTask();
+}
+
+onMounted(async () => {
+  await updateSetting();
+  await fetchTask(true, true);
+});
+
+const saveTaskToServer = async () => {
+  const mkey = "save_task_key";
+  void message.loading({ content: "标记评卷任务...", key: mkey });
+  const res = await saveInspectedTaskOfImportInspect(
+    currentStudentId + "",
+    !isCurrentTagged + ""
+  );
+  if (res.data.success) {
+    void message.success({
+      content: isCurrentTagged ? "取消标记成功" : "标记成功",
+      key: mkey,
+      duration: 2,
+    });
+    if (isCurrentTagged) {
+      tagIds.splice(tagIds.indexOf(currentStudentId), 1);
+    } else {
+      tagIds.push(currentStudentId);
+    }
+  } else {
+    console.log(res.data.message);
+    void message.error({ content: res.data.message, key: mkey, duration: 10 });
+  }
+};
+
+const renderError = () => {
+  store.currentTask = undefined;
+  store.message = "加载失败,请重新加载。";
+};
+</script>
+
+<style scoped>
+.my-container {
+  width: 100%;
+  overflow: clip;
+}
+</style>

+ 5 - 0
src/router/index.ts

@@ -16,6 +16,11 @@ const routes = [
     component: () =>
       import("@/features/student/importInspect/ImportInspect.vue"),
   },
+  {
+    //成绩校验
+    path: "/admin/exam/score/verify/start",
+    component: () => import("@/features/student/scoreVerify/ScoreVerify.vue"),
+  },
   {
     // 任务批量复核
     path: "/admin/exam/library/inspected/start",

+ 1 - 1
vite.config.ts

@@ -3,7 +3,7 @@ import vue from "@vitejs/plugin-vue";
 import ViteComponents from "unplugin-vue-components/vite";
 import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
 
-const SERVER_URL = "http://192.168.10.225";
+const SERVER_URL = "http://192.168.10.224";
 // const SERVER_URL = "http://192.168.11.103:8090";
 
 const path = require("path");