|
@@ -1,9 +1,542 @@
|
|
|
<template>
|
|
|
- <div></div>
|
|
|
+ <div class="arbitrate">
|
|
|
+ <div class="arbitrate-title">
|
|
|
+ <a-button class="arbitrate-back" @click="goback">
|
|
|
+ <template #icon><SwapLeftOutlined /> </template>后退
|
|
|
+ </a-button>
|
|
|
+ <div class="arbitrate-stat">
|
|
|
+ <p class="color-success">
|
|
|
+ <CheckCircleFilled />已处理:{{ progress.finishCount }}
|
|
|
+ </p>
|
|
|
+ <p class="color-error">
|
|
|
+ <CloseCircleFilled />未处理:{{ progress.todoCount }}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="arbitrate-body">
|
|
|
+ <RecognizeImage />
|
|
|
+
|
|
|
+ <!-- arbitrate action modal -->
|
|
|
+ <div v-if="curArbitrateTaskDetail" class="arbitrate-modal">
|
|
|
+ <a-row align="top" :gutter="8" class="m-b-8px">
|
|
|
+ <a-col :span="6">
|
|
|
+ <div class="modal-box">
|
|
|
+ <p class="box-title">客观题</p>
|
|
|
+ <p class="box-cont">{{ curTaskDetailName }}</p>
|
|
|
+ </div>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="12">
|
|
|
+ <div class="modal-box modal-origin"></div>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="6">
|
|
|
+ <div class="modal-box" @click="changePrevTaskDetail">
|
|
|
+ <p class="box-title">左键</p>
|
|
|
+ <p class="box-cont">上一个</p>
|
|
|
+ </div>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ <a-row align="top" :gutter="8">
|
|
|
+ <a-col :span="3">
|
|
|
+ <div class="modal-box">
|
|
|
+ <p class="box-title">一评结果</p>
|
|
|
+ <p class="box-cont">{{ curArbitrateTaskDetail.result1 }}</p>
|
|
|
+ </div>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="3">
|
|
|
+ <div class="modal-box">
|
|
|
+ <p class="box-title">二评结果</p>
|
|
|
+ <p class="box-cont">{{ curArbitrateTaskDetail.result2 }}</p>
|
|
|
+ </div>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="12">
|
|
|
+ <div class="modal-box modal-options">
|
|
|
+ <a-button
|
|
|
+ v-for="option in curArbitrateTaskDetail.options"
|
|
|
+ :key="option"
|
|
|
+ :type="
|
|
|
+ curArbitrateTaskDetail.value.result.includes(option)
|
|
|
+ ? 'primary'
|
|
|
+ : 'default'
|
|
|
+ "
|
|
|
+ @click="selectOption(option)"
|
|
|
+ >{{ option }}</a-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="6">
|
|
|
+ <div class="modal-box">
|
|
|
+ <p class="box-title">Enter键</p>
|
|
|
+ <p class="box-cont">下一个</p>
|
|
|
+ </div>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
+import { computed, ref, reactive, onMounted, onBeforeUnmount } from "vue";
|
|
|
+import { useRoute, useRouter } from "vue-router";
|
|
|
+import {
|
|
|
+ CheckCircleFilled,
|
|
|
+ CloseCircleFilled,
|
|
|
+ SwapLeftOutlined,
|
|
|
+} from "@ant-design/icons-vue";
|
|
|
+import { message } from "ant-design-vue";
|
|
|
+
|
|
|
+import RecognizeImage from "./RecognizeImage.vue";
|
|
|
+
|
|
|
+import {
|
|
|
+ recognizeArbitrateTask,
|
|
|
+ recognizeArbitrateSave,
|
|
|
+ recognizeArbitrateProgress,
|
|
|
+ recognizeArbitrateHistory,
|
|
|
+} from "@/ap/recognizeCheck";
|
|
|
+import {
|
|
|
+ RecognizeArbitrateItem,
|
|
|
+ RecognizeArbitrateTaskDetail,
|
|
|
+ RecogDataType,
|
|
|
+ RecogDataFillResult,
|
|
|
+ RecognizeArbitrateSavePage,
|
|
|
+} from "@/ap/types/recognizeCheck";
|
|
|
+import { maxNum, minNum } from "@/utils/tool";
|
|
|
+import { nextTick } from "vue";
|
|
|
+
|
|
|
defineOptions({
|
|
|
name: "RecognizeArbitrate",
|
|
|
});
|
|
|
+
|
|
|
+const router = useRouter();
|
|
|
+const route = useRoute();
|
|
|
+const groupId = route.params.groupId ? Number(route.params.groupId) : 0;
|
|
|
+
|
|
|
+// 任务进度
|
|
|
+const progress = ref({
|
|
|
+ finishCount: 0,
|
|
|
+ todoCount: 0,
|
|
|
+});
|
|
|
+async function updateProgress() {
|
|
|
+ const res = await recognizeArbitrateProgress(groupId);
|
|
|
+ progress.value = res || {};
|
|
|
+}
|
|
|
+
|
|
|
+const curArbitrateTaskDetails = ref([] as RecognizeArbitrateTaskDetail[]);
|
|
|
+const curArbitrateTaskDetailIndex = ref(0);
|
|
|
+const curTaskDetails = ref([] as RecognizeArbitrateTaskDetail[]);
|
|
|
+const curArbitrateTaskDetail = ref<RecognizeArbitrateTaskDetail | null>(null);
|
|
|
+
|
|
|
+// TODO: 缓存所有仲裁结果
|
|
|
+// const cacheArbitrateTaskDetailResults: Record<string, string[]> = {}
|
|
|
+
|
|
|
+const nextArbitrateTaskDetail = computed(() => {
|
|
|
+ return curArbitrateTaskDetails.value[curArbitrateTaskDetailIndex.value + 1];
|
|
|
+});
|
|
|
+// 下一个细分任务是否是另一个任务
|
|
|
+const nextDetailIsAnotherTask = computed(() => {
|
|
|
+ return !nextArbitrateTaskDetail.value;
|
|
|
+});
|
|
|
+// 下一个细分任务是否是另一页的
|
|
|
+const nextDetailIsAnotherPage = computed(() => {
|
|
|
+ if (!curArbitrateTaskDetail.value) return false;
|
|
|
+
|
|
|
+ return (
|
|
|
+ !nextArbitrateTaskDetail.value ||
|
|
|
+ nextArbitrateTaskDetail.value.pageIndex !==
|
|
|
+ curArbitrateTaskDetail.value.pageIndex
|
|
|
+ );
|
|
|
+});
|
|
|
+
|
|
|
+const curTaskDetailName = computed(() => {
|
|
|
+ if (!curArbitrateTaskDetail.value) return "";
|
|
|
+
|
|
|
+ if (curArbitrateTaskDetail.value.type === "question") {
|
|
|
+ return `#${curArbitrateTaskDetail.value.index}`;
|
|
|
+ }
|
|
|
+ return "-";
|
|
|
+});
|
|
|
+
|
|
|
+// 获取任务
|
|
|
+let curTask: RecognizeArbitrateItem | null = null;
|
|
|
+async function getTask() {
|
|
|
+ const res = await recognizeArbitrateTask(groupId).catch(() => false);
|
|
|
+ curTask = res || null;
|
|
|
+ if (!curTask) {
|
|
|
+ message.error("获取任务失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ curTaskDetails.value = parseDetails(curTask);
|
|
|
+ curArbitrateTaskDetails.value = curTaskDetails.value.filter(
|
|
|
+ (item) => item.arbitrate
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// 解析仲裁任务详情
|
|
|
+function parseDetails(
|
|
|
+ data: RecognizeArbitrateItem
|
|
|
+): RecognizeArbitrateTaskDetail[] {
|
|
|
+ const details: RecognizeArbitrateTaskDetail[] = [];
|
|
|
+ data.pages.forEach((page) => {
|
|
|
+ if (!page.recogData) return;
|
|
|
+ const precogData = window.atob(page.recogData);
|
|
|
+ const recogData: RecogDataType | null = precogData
|
|
|
+ ? JSON.parse(precogData)
|
|
|
+ : null;
|
|
|
+ if (!recogData) return;
|
|
|
+
|
|
|
+ // 缺考
|
|
|
+ details.push({
|
|
|
+ index: 0,
|
|
|
+ ...parseDetailSize(recogData.absent.fill_result[0], "absent"),
|
|
|
+ multiple: false,
|
|
|
+ result1: page.absent ? page.absent[0] : "",
|
|
|
+ result2: page.absent ? page.absent[1] : "",
|
|
|
+ pageIndex: page.index,
|
|
|
+ groupId: groupId,
|
|
|
+ arbitrate: Boolean(page.absent),
|
|
|
+ });
|
|
|
+
|
|
|
+ // 违纪
|
|
|
+ details.push({
|
|
|
+ index: 0,
|
|
|
+ ...parseDetailSize(recogData.breach.fill_result[0], "breach"),
|
|
|
+ multiple: false,
|
|
|
+ result1: page.breach ? page.breach[0] : "",
|
|
|
+ result2: page.breach ? page.breach[1] : "",
|
|
|
+ pageIndex: page.index,
|
|
|
+ groupId: groupId,
|
|
|
+ arbitrate: Boolean(page.breach),
|
|
|
+ });
|
|
|
+
|
|
|
+ // 试卷类型
|
|
|
+ details.push({
|
|
|
+ index: 0,
|
|
|
+ ...parseDetailSize(recogData.paperType.fill_result[0], "paperType"),
|
|
|
+ multiple: false,
|
|
|
+ result1: page.paperType ? page.paperType[0] : "",
|
|
|
+ result2: page.paperType ? page.paperType[1] : "",
|
|
|
+ pageIndex: page.index,
|
|
|
+ groupId: groupId,
|
|
|
+ arbitrate: Boolean(page.paperType),
|
|
|
+ });
|
|
|
+
|
|
|
+ // 试题
|
|
|
+ let index = 0;
|
|
|
+ recogData.question.forEach((gGroup) => {
|
|
|
+ gGroup.fill_result.forEach((qRecog) => {
|
|
|
+ qRecog.index = ++index;
|
|
|
+ const questionResult = page.question[qRecog.index];
|
|
|
+ const arbitrate = questionResult && questionResult.length >= 2;
|
|
|
+
|
|
|
+ details.push({
|
|
|
+ index: qRecog.index,
|
|
|
+ ...parseDetailSize(qRecog, "question"),
|
|
|
+ multiple: true,
|
|
|
+ result: arbitrate ? [] : questionResult,
|
|
|
+ result1: questionResult ? questionResult[0] : "",
|
|
|
+ result2: questionResult ? questionResult[1] : "",
|
|
|
+ pageIndex: page.index,
|
|
|
+ groupId: groupId,
|
|
|
+ arbitrate,
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ return details;
|
|
|
+}
|
|
|
+
|
|
|
+const abc = "abcdefghijklmnopqrstuvwxyz".toUpperCase();
|
|
|
+type DetailSizeResult = Pick<
|
|
|
+ RecognizeArbitrateTaskDetail,
|
|
|
+ "fillPosition" | "fillSize" | "fillArea" | "options" | "type" | "result"
|
|
|
+>;
|
|
|
+function parseDetailSize(
|
|
|
+ data: RecogDataFillResult,
|
|
|
+ type: string
|
|
|
+): DetailSizeResult {
|
|
|
+ const result: DetailSizeResult = {
|
|
|
+ type,
|
|
|
+ fillPosition: [],
|
|
|
+ fillSize: { w: 0, h: 0 },
|
|
|
+ fillArea: { x: 0, y: 0, w: 0, h: 0 },
|
|
|
+ options: [],
|
|
|
+ result: [],
|
|
|
+ };
|
|
|
+
|
|
|
+ if (!data) return result;
|
|
|
+
|
|
|
+ result.fillPosition = data.fill_position.map((item) => {
|
|
|
+ const size = item.split(",");
|
|
|
+ return {
|
|
|
+ x: size[0] ? Number(size[0]) : 0,
|
|
|
+ y: size[1] ? Number(size[1]) : 0,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ result.fillSize = {
|
|
|
+ w: data.fill_size[0],
|
|
|
+ h: data.fill_size[1],
|
|
|
+ };
|
|
|
+ const xs = result.fillPosition.map((item) => item.x);
|
|
|
+ const maxX = maxNum(xs);
|
|
|
+ const minX = minNum(xs);
|
|
|
+ result.fillArea = {
|
|
|
+ x: minX + result.fillSize.w + 10,
|
|
|
+ y: result.fillPosition[0].y - 5,
|
|
|
+ w: maxX - minX + result.fillSize.w + 20,
|
|
|
+ h: result.fillPosition[0].y + result.fillSize.h + 5,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (type === "question") {
|
|
|
+ const options = abc.substring(0, data.fill_position.length).split("");
|
|
|
+ // 空用“#”表示
|
|
|
+ result.options = ["#", ...options];
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+// 任务执行流程 ----------------- start>
|
|
|
+function setCurTaskDetail() {
|
|
|
+ curArbitrateTaskDetail.value =
|
|
|
+ curArbitrateTaskDetails.value[curArbitrateTaskDetailIndex.value];
|
|
|
+}
|
|
|
+const detailChanging = ref(false);
|
|
|
+async function changeNextTaskDetail() {
|
|
|
+ if (detailChanging.value) return;
|
|
|
+ detailChanging.value = true;
|
|
|
+
|
|
|
+ if (nextDetailIsAnotherTask.value) {
|
|
|
+ await submitCurTask().catch(() => {});
|
|
|
+ await getNextTask().catch(() => {});
|
|
|
+ await nextTick(() => {
|
|
|
+ detailChanging.value = false;
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ curArbitrateTaskDetailIndex.value++;
|
|
|
+ setCurTaskDetail();
|
|
|
+ setTimeout(() => {
|
|
|
+ detailChanging.value = false;
|
|
|
+ }, 500);
|
|
|
+}
|
|
|
+
|
|
|
+async function changePrevTaskDetail() {
|
|
|
+ if (detailChanging.value) return;
|
|
|
+
|
|
|
+ if (curArbitrateTaskDetailIndex.value === 0) {
|
|
|
+ detailChanging.value = true;
|
|
|
+ await getPrevTask().catch(() => {});
|
|
|
+ await nextTick(() => {
|
|
|
+ detailChanging.value = false;
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ detailChanging.value = true;
|
|
|
+ curArbitrateTaskDetailIndex.value--;
|
|
|
+ setCurTaskDetail();
|
|
|
+ setTimeout(() => {
|
|
|
+ detailChanging.value = false;
|
|
|
+ }, 500);
|
|
|
+}
|
|
|
+
|
|
|
+async function getNextTask() {
|
|
|
+ let result = true;
|
|
|
+ const res = await recognizeArbitrateHistory({
|
|
|
+ groupId,
|
|
|
+ id: curTask?.id,
|
|
|
+ }).catch(() => {
|
|
|
+ result = false;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!result) {
|
|
|
+ message.error("获取上一个任务失败!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!res) {
|
|
|
+ message.error("没有上一个任务了!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ curTask = res;
|
|
|
+ curTaskDetails.value = parseDetails(curTask);
|
|
|
+ curArbitrateTaskDetails.value = curTaskDetails.value.filter(
|
|
|
+ (item) => item.arbitrate
|
|
|
+ );
|
|
|
+
|
|
|
+ curArbitrateTaskDetailIndex.value = curArbitrateTaskDetails.value.length - 1;
|
|
|
+ setCurTaskDetail();
|
|
|
+}
|
|
|
+
|
|
|
+async function getPrevTask() {
|
|
|
+ let result = true;
|
|
|
+ const res = await recognizeArbitrateHistory({
|
|
|
+ groupId,
|
|
|
+ id: curTask?.id,
|
|
|
+ next: true,
|
|
|
+ }).catch(() => {
|
|
|
+ result = false;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!result) {
|
|
|
+ message.error("获取下一个任务失败!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!res) {
|
|
|
+ message.error("没有下一个任务了!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ curTaskDetails.value = parseDetails(curTask);
|
|
|
+ curArbitrateTaskDetails.value = curTaskDetails.value.filter(
|
|
|
+ (item) => item.arbitrate
|
|
|
+ );
|
|
|
+
|
|
|
+ curArbitrateTaskDetailIndex.value = 0;
|
|
|
+ setCurTaskDetail();
|
|
|
+}
|
|
|
+
|
|
|
+function selectOption(option: string) {
|
|
|
+ if (!curArbitrateTaskDetail.value) return;
|
|
|
+
|
|
|
+ // 单选直接赋值
|
|
|
+ if (!curArbitrateTaskDetail.value.multiple) {
|
|
|
+ curArbitrateTaskDetail.value.result = [option];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 多选情况
|
|
|
+ // 空直接赋值,空值与其他互斥
|
|
|
+ if (option === "#") {
|
|
|
+ curArbitrateTaskDetail.value.result = ["#"];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = curArbitrateTaskDetail.value.result.filter(
|
|
|
+ (item) => item === "#"
|
|
|
+ );
|
|
|
+ if (result.includes(option)) {
|
|
|
+ result = result.filter((item) => item !== option);
|
|
|
+ } else {
|
|
|
+ result.push(option);
|
|
|
+ }
|
|
|
+ // 保证result的顺序和options的顺序是一致的
|
|
|
+ curArbitrateTaskDetail.value.result =
|
|
|
+ curArbitrateTaskDetail.value.options.filter((item) =>
|
|
|
+ result.includes(item)
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// 键盘事件
|
|
|
+function registKeyEvent() {
|
|
|
+ document.addEventListener("keydown", keyEventHandle);
|
|
|
+}
|
|
|
+function removeKeyEvent() {
|
|
|
+ document.removeEventListener("keydown", keyEventHandle);
|
|
|
+}
|
|
|
+
|
|
|
+function keyEventHandle(e: KeyboardEvent) {
|
|
|
+ if (e.code === "Enter") {
|
|
|
+ e.preventDefault();
|
|
|
+ onConfirm();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 保存任务详情信息
|
|
|
+async function onConfirm() {
|
|
|
+ if (!curArbitrateTaskDetail.value?.result.length) {
|
|
|
+ message.error("请完成仲裁结果!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await changeNextTaskDetail();
|
|
|
+}
|
|
|
+
|
|
|
+// 提交当前任务仲裁数据
|
|
|
+function getTaskTypeResult(
|
|
|
+ type: "absent" | "breach" | "paperType",
|
|
|
+ pageIndex: number
|
|
|
+) {
|
|
|
+ const taskDetail = curArbitrateTaskDetails.value.find(
|
|
|
+ (item) => item.pageIndex === pageIndex && item.type === type
|
|
|
+ );
|
|
|
+
|
|
|
+ const result = taskDetail ? taskDetail.result.join() : null;
|
|
|
+
|
|
|
+ if (type === "paperType") return result;
|
|
|
+
|
|
|
+ return result ? result === "true" : null;
|
|
|
+}
|
|
|
+
|
|
|
+async function submitCurTask() {
|
|
|
+ if (!curTask) return;
|
|
|
+ const curArbitrateTaskDetailQuestionResults: Record<string, string> = {};
|
|
|
+ curArbitrateTaskDetails.value.forEach((item) => {
|
|
|
+ if (item.type !== "question") return;
|
|
|
+
|
|
|
+ const k = `${item.pageIndex}-${item.index}`;
|
|
|
+ curArbitrateTaskDetailQuestionResults[k] = item.result.join("");
|
|
|
+ });
|
|
|
+
|
|
|
+ const pages = curTask.pages.map((page) => {
|
|
|
+ const absentResult = getTaskTypeResult(
|
|
|
+ "absent",
|
|
|
+ page.index
|
|
|
+ ) as RecognizeArbitrateSavePage["absent"];
|
|
|
+ const breachResult = getTaskTypeResult(
|
|
|
+ "breach",
|
|
|
+ page.index
|
|
|
+ ) as RecognizeArbitrateSavePage["breach"];
|
|
|
+
|
|
|
+ const paperTypeResult = getTaskTypeResult(
|
|
|
+ "paperType",
|
|
|
+ page.index
|
|
|
+ ) as RecognizeArbitrateSavePage["paperType"];
|
|
|
+
|
|
|
+ const questionResult: Record<number, string> = {};
|
|
|
+ curTaskDetails.value.forEach((item) => {
|
|
|
+ if (item.type !== "question") return;
|
|
|
+
|
|
|
+ const k = `${item.pageIndex}-${item.index}`;
|
|
|
+ const result = item.arbitrate
|
|
|
+ ? curArbitrateTaskDetailQuestionResults[k]
|
|
|
+ : item.result.join("");
|
|
|
+ questionResult[item.index] = result;
|
|
|
+ });
|
|
|
+
|
|
|
+ const npage: RecognizeArbitrateSavePage = {
|
|
|
+ index: page.index,
|
|
|
+ absent: absentResult,
|
|
|
+ breach: breachResult,
|
|
|
+ paperType: paperTypeResult,
|
|
|
+ question: questionResult,
|
|
|
+ selective: null,
|
|
|
+ };
|
|
|
+ return npage;
|
|
|
+ });
|
|
|
+
|
|
|
+ await recognizeArbitrateSave({ id: curTask.id, pages }).catch(() => false);
|
|
|
+}
|
|
|
+
|
|
|
+// 任务执行流程 ----------------- end>
|
|
|
+
|
|
|
+// 返回
|
|
|
+function goback() {
|
|
|
+ router.back();
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ registKeyEvent();
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ removeKeyEvent();
|
|
|
+});
|
|
|
</script>
|