浏览代码

feat: 审核员复核校验

zhangjie 9 月之前
父节点
当前提交
4fb03eea6c

+ 55 - 1
src/render/ap/review.ts

@@ -24,7 +24,7 @@ export const reviewTask = (
     data,
   });
 
-// todo:复核校验进度状态
+// 复核校验进度状态
 export const reviewProgress = (
   data: ExamSubjectParams
 ): Promise<ReviewProgressResult> =>
@@ -96,3 +96,57 @@ export const reviewWarningTaskExport = (
     data,
     download: true,
   });
+
+// 审核员复核 -------------------->
+// 复核任务
+export const reviewAuditTask = (
+  data: ExamSubjectParams
+): Promise<ReviewTaskListItem> =>
+  request({
+    url: "/api/auditor/check/assigned/task/get",
+    method: "post",
+    data,
+  });
+
+// 复核校验进度状态
+export const reviewAuditProgress = (
+  data: ExamSubjectParams
+): Promise<ReviewProgressResult> =>
+  request({
+    url: "/api/auditor/check/assigned/overview",
+    method: "post",
+    data,
+  });
+
+// 复核校验结果提交
+export const reviewAuditTaskSave = (
+  data: ReviewTaskSaveParams
+): Promise<RequestActionResult> =>
+  request({
+    url: "/api/auditor/check/assigned/task/save",
+    method: "post",
+    data,
+    headers: {
+      "Content-Type": "application/json;charset=UTF-8",
+    },
+  });
+
+// 复核校验任务历史
+export const reviewAuditTaskHistory = (
+  data: ExamParams
+): Promise<ReviewTaskListItem[]> =>
+  request({
+    url: "/api/auditor/check/assigned/task/history",
+    method: "post",
+    data,
+  });
+
+// 复核校验任务释放
+export const reviewAuditTaskRelease = (
+  data: ExamParams
+): Promise<{ success: boolean }> =>
+  request({
+    url: "/api/auditor/check/assigned/task/release",
+    method: "post",
+    data,
+  });

+ 1 - 1
src/render/router/routes.ts

@@ -120,7 +120,7 @@ const routes: RouteRecordRaw[] = [
       },
       // 实时审核
       {
-        path: "in-time-audit",
+        path: "intime-audit",
         name: "IntimeAudit",
         component: () => import("@/views/Audit/Intime/index.vue"),
         meta: {

+ 201 - 0
src/render/views/Audit/Review/ReviewAction.vue

@@ -0,0 +1,201 @@
+<template>
+  <div class="review-action">
+    <div class="review-tabs">
+      <div
+        :class="[
+          'review-tab',
+          { 'is-active': reviewStore.tabKey === 'review' },
+        ]"
+        @click="switchTab('review')"
+      >
+        复核校验
+      </div>
+      <div
+        :class="[
+          'review-tab',
+          { 'is-active': reviewStore.tabKey === 'history' },
+        ]"
+        @click="switchTab('history')"
+      >
+        历史记录
+      </div>
+    </div>
+
+    <div v-show="reviewStore.tabKey === 'review'" class="review-tbody">
+      <a-collapse v-model:activeKey="reviewKey" :bordered="false">
+        <a-collapse-panel key="1">
+          <template #header><FilterFilled />搜索条件 </template>
+          <span>科目:</span>
+          <a-select
+            v-model:value="searchCourseCode"
+            placeholder="请选择科目"
+            :options="courses"
+            :field-names="fieldNames"
+            filter-option
+            style="width: 140px"
+          ></a-select>
+          <a-button
+            class="m-l-8px"
+            type="primary"
+            :disabled="!searchCourseCode"
+            @click="onSearch"
+            >搜索</a-button
+          >
+        </a-collapse-panel>
+        <a-collapse-panel key="2">
+          <template #header><PushpinFilled />复核标记 </template>
+
+          <a-radio-group
+            v-if="reviewStore.curTask"
+            v-model:value="result"
+            @change="onMark"
+          >
+            <a-radio :value="1">正常</a-radio>
+            <a-radio :value="0">异常</a-radio>
+          </a-radio-group>
+        </a-collapse-panel>
+      </a-collapse>
+    </div>
+    <div
+      v-show="reviewStore.tabKey === 'history'"
+      class="review-tbody tbody-history"
+    >
+      <a-collapse :activeKey="['1']" :bordered="false">
+        <a-collapse-panel key="1">
+          <template #header><PushpinFilled />复核标记 </template>
+
+          <a-radio-group
+            v-if="reviewStore.curTask"
+            v-model:value="historyResult"
+            @change="onMark"
+          >
+            <a-radio :value="1">正常</a-radio>
+            <a-radio :value="0">异常</a-radio>
+          </a-radio-group>
+        </a-collapse-panel>
+      </a-collapse>
+      <div class="history-list">
+        <div class="task-list">
+          <ul class="list-head">
+            <li class="li-grow">准考证号</li>
+            <li style="width: 80px">状态</li>
+          </ul>
+          <div class="list-body">
+            <ul
+              v-for="(item, index) in dataList"
+              :key="item.examNumber"
+              :class="[
+                'list-row',
+                { 'is-active': reviewStore.curTask?.id === item.id },
+              ]"
+              @click="setCurTask(index)"
+            >
+              <li class="li-grow">{{ item.examNumber }}</li>
+              <li style="width: 80px">
+                <span
+                  :class="
+                    item.assignedSuspect ? 'color-success' : 'color-error'
+                  "
+                >
+                  {{ item.assignedSuspect ? "正常" : "异常" }}
+                </span>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from "vue";
+import { FilterFilled, PushpinFilled } from "@ant-design/icons-vue";
+import { message } from "ant-design-vue";
+
+import { getSubjectList } from "@/ap/base";
+import { reviewAuditTaskHistory } from "@/ap/review";
+import { ReviewTaskListItem } from "@/ap/types/review";
+import { SubjectItem } from "@/ap/types/base";
+
+import { useUserStore, useReviewStore } from "@/store";
+
+defineOptions({
+  name: "ReviewAction",
+});
+const emit = defineEmits(["search", "mark"]);
+defineExpose({ switchTab });
+
+const userStore = useUserStore();
+const reviewStore = useReviewStore();
+
+const fieldNames = { label: "name", value: "code" };
+
+// tab
+const reviewKey = ref(["1", "2"]);
+
+async function switchTab(key: "review" | "history") {
+  reviewStore.setInfo({ tabKey: key });
+
+  if (key === "history") {
+    reviewStore.setInfo({ waitTask: reviewStore.curTask });
+    await getHistory();
+    setCurTask(0);
+  } else {
+    reviewStore.setInfo({ curTask: reviewStore.waitTask, waitTask: null });
+  }
+}
+
+// course data
+const courses = ref<SubjectItem[]>([]);
+async function getCourses() {
+  const res = await getSubjectList({ examId: userStore.curExam.id });
+  courses.value = res || [];
+}
+getCourses();
+
+const searchCourseCode = ref("");
+const result = ref();
+const historyResult = ref();
+
+// history
+const curHistoryTaskIndex = ref(0);
+const dataList = ref<ReviewTaskListItem[]>([]);
+async function getHistory() {
+  if (!reviewStore.waitTask?.id) return;
+  const res = await reviewAuditTaskHistory({
+    id: reviewStore.waitTask?.id,
+    subjectCode: searchCourseCode.value,
+    pageSize: 30,
+  });
+  dataList.value = res || [];
+}
+
+function setCurTask(index: number) {
+  curHistoryTaskIndex.value = index;
+  reviewStore.setInfo({ curTask: dataList.value[index] });
+}
+
+// actions
+function onSearch() {
+  emit("search", searchCourseCode.value);
+}
+
+function onMark() {
+  emit(
+    "mark",
+    reviewStore.tabKey === "review" ? result.value : historyResult.value
+  );
+}
+
+// watch
+watch(
+  () => reviewStore.curTask,
+  (val) => {
+    result.value = undefined;
+    if (reviewStore.tabKey === "history") {
+      historyResult.value = reviewStore.curTask?.assignedSuspect;
+    }
+  }
+);
+</script>

+ 140 - 1
src/render/views/Audit/Review/index.vue

@@ -1,9 +1,148 @@
 <template>
-  <div>ReviewAudit 复用Review,通过角色区分页面差异</div>
+  <div class="review">
+    <div class="review-head">
+      <h2 v-if="reviewStore.curTask" class="review-title">
+        {{ reviewStore.curTask.examNumber }} - {{ reviewStore.curTask.name }} -
+        {{ userStore.curExam?.name }}
+      </h2>
+
+      <div class="review-stat">
+        <p class="color-success">
+          <CheckCircleFilled />已处理:{{ progress.finishCount }}
+        </p>
+        <p class="color-error">
+          <CloseCircleFilled />未处理:{{ progress.todoCount }}
+        </p>
+      </div>
+
+      <div class="review-prev">
+        <a-button
+          v-if="reviewStore.tabKey === 'review'"
+          :disabled="loading"
+          @click="onPrevTask"
+        >
+          <template #icon><ArrowLeftOutlined /></template> 上一个
+        </a-button>
+      </div>
+    </div>
+
+    <!-- body -->
+    <div class="review-body">
+      <ReviewImage />
+
+      <!-- ReviewMarkPan -->
+      <ReviewMarkPan
+        v-if="reviewStore.curTask"
+        :task-info="{
+          examNumber: reviewStore.curTask.examNumber,
+          name: reviewStore.curTask.name,
+        }"
+        @mark="onMark"
+      />
+    </div>
+
+    <!-- action -->
+    <ReviewAction ref="reviewActionRef" @mark="onMark" @search="onSearch" />
+  </div>
 </template>
 
 <script setup lang="ts">
+import { ref, reactive, onBeforeUnmount, onMounted } from "vue";
+import {
+  CheckCircleFilled,
+  CloseCircleFilled,
+  ArrowLeftOutlined,
+} from "@ant-design/icons-vue";
+import { message } from "ant-design-vue";
+
+import {
+  reviewAuditTask,
+  reviewAuditTaskSave,
+  reviewAuditProgress,
+  reviewAuditTaskRelease,
+} from "@/ap/review";
+import { ReviewTaskListItem } from "@/ap/types/review";
+import { SubjectItem } from "@/ap/types/base";
+
+import { useUserStore, useReviewStore } from "@/store";
+import useLoading from "@/hooks/useLoading";
+
+import ReviewAction from "./ReviewAction.vue";
+import ReviewImage from "../../Review/ReviewImage.vue";
+import ReviewMarkPan from "../../Review/ReviewMarkPan.vue";
+
+const userStore = useUserStore();
+const reviewStore = useReviewStore();
+
 defineOptions({
   name: "ReviewAudit",
 });
+
+// 任务相关
+const searchModel = reactive({
+  examId: userStore.curExam.id,
+  subjectCode: "",
+});
+
+const { loading, setLoading } = useLoading();
+
+async function getNextTask() {
+  const res = await reviewAuditTask(searchModel);
+  reviewStore.setInfo({ curTask: res || null });
+
+  if (!reviewStore.curTask) {
+    message.error("没有下一个了!");
+    return;
+  }
+}
+
+const reviewActionRef = ref();
+async function onPrevTask() {
+  reviewActionRef.value?.switchTab("history");
+}
+
+// 任务进度
+const progress = ref({
+  finishCount: 0,
+  todoCount: 0,
+});
+async function updateProgress() {
+  const res = await reviewAuditProgress(searchModel);
+  progress.value = res || {};
+}
+
+// actions
+async function onMark(assignedSuspect: boolean) {
+  if (!reviewStore.curTask) return;
+
+  if (loading.value) return;
+  loading.value = true;
+
+  try {
+    await reviewAuditTaskSave({ id: reviewStore.curTask.id, assignedSuspect });
+    if (reviewStore.tabKey === "history") return;
+
+    await getNextTask();
+  } catch (error) {
+    loading.value = false;
+  }
+}
+
+async function onSearch(subjectCode: string) {
+  searchModel.subjectCode = subjectCode;
+  await getNextTask();
+  await updateProgress();
+}
+
+async function releaseTask() {
+  await reviewAuditTaskRelease({ examId: userStore.curExam.id });
+}
+
+onMounted(async () => {
+  reviewStore.resetInfo();
+  await releaseTask();
+});
+onBeforeUnmount(async () => {
+  await releaseTask();
+});
 </script>

+ 0 - 2
src/render/views/Review/ReviewAction.vue

@@ -169,11 +169,9 @@ import { reviewWarningTaskExport, reviewTaskHistory } from "@/ap/review";
 import { ReviewTaskListItem, ReviewExportType } from "@/ap/types/review";
 import { SubjectItem } from "@/ap/types/base";
 
-import useTable from "@/hooks/useTable";
 import useLoading from "@/hooks/useLoading";
 import { useUserStore, useReviewStore } from "@/store";
 
-import SimplePagination from "@/components/SimplePagination/index.vue";
 import ExportTypeDialog from "./ExportTypeDialog.vue";
 
 defineOptions({

+ 1 - 0
src/render/views/Review/ReviewMarkPan.vue

@@ -92,6 +92,7 @@ function moveAreaStop(pos: MovePos) {
 
 function updateStaticSize() {
   const panDom = reviewMarkPanRef.value as HTMLDivElement;
+  if (!panDom) return;
   panWidth = panDom.clientWidth;
   panHeight = panDom.clientHeight;
   const parentDom = panDom.parentElement as HTMLDivElement;