Procházet zdrojové kódy

feat: api-质量监控

zhangjie před 1 týdnem
rodič
revize
f44c4226a4

+ 37 - 4
src/api/mark.ts

@@ -1,10 +1,9 @@
-import axios from 'axios';
+import axios, { AxiosResponse } from 'axios';
 import {
   MarkQualityMonitorListFilter,
   MarkQualityMonitorListPageParam,
   MarkQualityMonitorListPageRes,
   QMScoreItem,
-  QMScoreListParam,
   MarkArbitrationListPageRes,
   MarkArbitrationListPageParam,
   MarkTaskListPageRes,
@@ -20,6 +19,9 @@ import {
   MarkStatListPageParam,
   MarkStatListPageRes,
   MarkRejectResetTaskParam,
+  GroupArbitrateStatFilter,
+  GroupArbitrateStatListRes,
+  GroupArbitrateStatListParam,
 } from './types/mark';
 
 // 质量监控
@@ -27,8 +29,39 @@ import {
 export function getQualityMonitorList(
   params: MarkQualityMonitorListPageParam
 ): Promise<MarkQualityMonitorListPageRes> {
-  return axios.post('/api/admin/exam/quality/list/group', {}, { params });
+  return axios.post('/api/admin/exam/quality/list/marker', {}, { params });
 }
+
+// 导出质量监控列表
+export function exportQualityMonitorList(
+  params: MarkQualityMonitorListFilter
+): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/exam/quality/export',
+    {},
+    { params, responseType: 'blob' }
+  );
+}
+
+// 分组统计仲裁列表
+export function getGroupArbitrateStatList(
+  params: GroupArbitrateStatListParam
+): Promise<GroupArbitrateStatListRes> {
+  const url = params.group
+    ? '/api/admin/exam/quality/list/group'
+    : '/api/admin/exam/quality/list/subject';
+  return axios.post(url, {}, { params });
+}
+// 分组统计仲裁导出
+export function exportGroupArbitrateStatList(
+  params: GroupArbitrateStatFilter
+): Promise<AxiosResponse<Blob>> {
+  const url = params.group
+    ? '/api/admin/exam/quality/list/group/export'
+    : '/api/admin/exam/quality/list/subject/export';
+  return axios.post(url, {}, { params, responseType: 'blob' });
+}
+
 // 重新计算
 export function qualityMonitorCalculate(
   params: MarkQualityMonitorListFilter
@@ -37,7 +70,7 @@ export function qualityMonitorCalculate(
 }
 // 给分曲线
 export function qualityMonitorScoreList(
-  params: QMScoreListParam
+  params: MarkQualityMonitorListFilter
 ): Promise<QMScoreItem[]> {
   return axios.post('/api/admin/exam/quality/chart', {}, { params });
 }

+ 31 - 6
src/api/types/mark.ts

@@ -6,6 +6,37 @@ import {
 import { PageResult, PageParams, CoverArea } from './common';
 
 // 质量监控
+// 分组统计仲裁:科目 分组 任务总量 已完成 仲裁卷数 仲裁率 已完成仲裁 待处理仲裁
+export interface GroupArbitrateStatItem {
+  // 科目
+  subjectCode: string;
+  subjectName: string;
+  // 分组
+  groupNumber: string;
+  groupName: string;
+  groupText: string;
+  // 任务总量
+  totalCount: number;
+  // 已完成
+  finishCount: number;
+  // 仲裁卷数
+  arbitrateCount: number;
+  // 仲裁率
+  arbitrateRatio: number;
+  // 已完成仲裁
+  finishArbitrateCount: number;
+  // 待处理仲裁
+  waitArbitrateCount: number;
+}
+export type GroupArbitrateStatListRes = PageResult<GroupArbitrateStatItem>;
+export interface GroupArbitrateStatFilter {
+  // 科目
+  subjectCode: string | null;
+  // 分组
+  group?: boolean;
+}
+export type GroupArbitrateStatListParam = PageParams<GroupArbitrateStatFilter>;
+
 // 质量监控列表:分组	评卷员	姓名	完成任务数	仲裁任务数	仲裁率	打回次数	评卷采用率	评卷速度(秒)	平均分	标准差
 export interface MarkQualityMonitorItem {
   // 分组
@@ -57,12 +88,6 @@ export interface QMScoreItem {
   // 给分
   scores: Array<{ score: number; count: number }>;
 }
-export type QMScoreListParam = {
-  // 科目
-  subjectCode: string;
-  // 分组
-  groupNumber: string;
-};
 
 // 仲裁管理
 // 仲裁管理列表:科目代码	分组序号	准考证号	状态	创建时间	处理时间	处理人

+ 4 - 2
src/router/routes/modules/base.ts

@@ -299,7 +299,8 @@ const BASE: AppRouteRecordRaw = {
         {
           path: '/quality-monitor',
           name: 'QualityMonitor',
-          component: () => import('@/views/mark/QualityMonitor.vue'),
+          component: () =>
+            import('@/views/mark/quality-monitor/QualityMonitor.vue'),
           meta: {
             title: '质量监控',
             requiresAuth: true,
@@ -310,7 +311,8 @@ const BASE: AppRouteRecordRaw = {
         {
           path: '/quality-monitor/score-curve',
           name: 'ScoreCurve',
-          component: () => import('@/views/mark/ScoreCurve.vue'),
+          component: () =>
+            import('@/views/mark/quality-monitor/ScoreCurve.vue'),
           meta: {
             title: '给分曲线',
             requiresAuth: true,

+ 9 - 0
src/utils/download-export.ts

@@ -32,6 +32,10 @@ import {
   exportQuestionAnalysisList,
   exportBigQuestionAnalysisList,
 } from '@/api/analysis';
+import {
+  exportQualityMonitorList,
+  exportGroupArbitrateStatList,
+} from '@/api/mark';
 
 import useLoading from '@/hooks/loading';
 
@@ -100,6 +104,11 @@ const downloadConfig = {
   exportQuestionAnalysisList,
   // 大题分析导出
   exportBigQuestionAnalysisList,
+  // mark
+  // 分组仲裁统计导出
+  exportGroupArbitrateStatList,
+  // 导出质量监控列表
+  exportQualityMonitorList,
 };
 type DownloadType = keyof typeof downloadConfig;
 

+ 49 - 0
src/views/mark/quality-monitor/QualityMonitor.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="quality-monitor-container">
+    <!-- 分组统计仲裁 -->
+    <QualityMonitorGroup class="monitor-section" />
+
+    <!-- 评卷员质量监控 -->
+    <QualityMonitorMarker class="monitor-section" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import QualityMonitorGroup from './QualityMonitorGroup.vue';
+  import QualityMonitorMarker from './QualityMonitorMarker.vue';
+
+  defineOptions({
+    name: 'QualityMonitor',
+  });
+</script>
+
+<style scoped>
+  .quality-monitor-container {
+    display: flex;
+    flex-direction: column;
+    height: calc(100vh - 56px);
+    gap: 16px;
+    padding: 16px;
+    overflow: hidden;
+  }
+
+  .monitor-section {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    min-height: 0;
+    background: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+
+  .monitor-section :deep(.page-table) {
+    flex: 1;
+    overflow: auto;
+  }
+
+  .monitor-section :deep(.el-pagination) {
+    margin-top: 16px;
+    justify-content: center;
+  }
+</style>

+ 117 - 0
src/views/mark/quality-monitor/QualityMonitorGroup.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="part-box is-border">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subjectCode"></select-subject>
+      </el-form-item>
+      <el-form-item label="分组">
+        <el-checkbox v-model="searchModel.group"> 按分组统计 </el-checkbox>
+      </el-form-item>
+    </el-form>
+    <el-divider class="form-divider" />
+    <div class="part-action">
+      <el-space wrap>
+        <el-button type="primary" @click="toPage(1)">查询</el-button>
+        <el-button @click="exportData">导出</el-button>
+      </el-space>
+    </div>
+
+    <el-table
+      class="page-table"
+      :data="dataList"
+      :loading="loading"
+      border
+      stripe
+      @sort-change="handleSortChange"
+    >
+      <el-table-column
+        prop="subjectCode"
+        label="科目"
+        min-width="200"
+        sortable
+      />
+      <el-table-column
+        v-if="searchModel.group"
+        prop="groupNumber"
+        label="分组"
+        width="80"
+      />
+      <el-table-column
+        prop="totalCount"
+        label="任务总量"
+        width="120"
+        sortable
+      />
+      <el-table-column prop="finishCount" label="已完成" width="100" sortable />
+      <el-table-column
+        prop="arbitrateCount"
+        label="仲裁卷数"
+        width="120"
+        sortable
+      />
+      <el-table-column label="仲裁率" width="100" sortable>
+        <template #default="scope">
+          {{ (scope.row.arbitrateRatio * 100).toFixed(1) }}%
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="finishArbitrateCount"
+        label="已完成仲裁"
+        width="120"
+        sortable
+      />
+      <el-table-column
+        prop="waitArbitrateCount"
+        label="待处理仲裁"
+        width="120"
+        sortable
+      />
+    </el-table>
+    <el-pagination
+      v-model:current-page="pagination.pageNumber"
+      v-model:page-size="pagination.pageSize"
+      :layout="pagination.layout"
+      :total="pagination.total"
+      @size-change="pageSizeChange"
+      @current-change="toPage"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive } from 'vue';
+  import { getGroupArbitrateStatList } from '@/api/mark';
+  import {
+    GroupArbitrateStatItem,
+    GroupArbitrateStatFilter,
+  } from '@/api/types/mark';
+  import useTable from '@/hooks/table';
+  import { downloadExport } from '@/utils/download-export';
+
+  defineOptions({
+    name: 'QualityMonitorGroup',
+  });
+
+  const searchModel = reactive<GroupArbitrateStatFilter>({
+    subjectCode: null,
+    group: false,
+  });
+
+  const {
+    dataList,
+    pagination,
+    loading,
+    toPage,
+    pageSizeChange,
+    handleSortChange,
+  } = useTable<GroupArbitrateStatItem>(
+    getGroupArbitrateStatList,
+    searchModel,
+    false
+  );
+
+  // 导出功能
+  async function exportData() {
+    await downloadExport('exportGroupArbitrateStatList', searchModel);
+  }
+</script>

+ 10 - 5
src/views/mark/QualityMonitor.vue → src/views/mark/quality-monitor/QualityMonitorMarker.vue

@@ -28,6 +28,7 @@
           >重新计算</el-button
         >
         <el-button @click="onViewScoreCurve">给分曲线</el-button>
+        <el-button @click="exportData">导出</el-button>
       </el-space>
     </div>
 
@@ -109,9 +110,11 @@
   } from '@/api/types/mark';
   import useTable from '@/hooks/table';
   import useLoading from '@/hooks/loading';
+  import { ls } from '@/utils/storage';
+  import { downloadExport } from '@/utils/download-export';
 
   defineOptions({
-    name: 'QualityMonitor',
+    name: 'QualityMonitorMarker',
   });
 
   const router = useRouter();
@@ -153,13 +156,15 @@
   }
 
   function onViewScoreCurve() {
+    ls.set('score-curve', searchModel);
     // 跳转到给分曲线页面,传递当前的筛选条件
     router.push({
       path: '/quality-monitor/score-curve',
-      query: {
-        subjectCode: searchModel.subjectCode || '',
-        groupNumber: searchModel.groupNumber || '',
-      },
     });
   }
+
+  // 导出功能
+  async function exportData() {
+    await downloadExport('exportQualityMonitorList', searchModel);
+  }
 </script>

+ 13 - 9
src/views/mark/ScoreCurve.vue → src/views/mark/quality-monitor/ScoreCurve.vue

@@ -41,27 +41,31 @@
 
 <script setup lang="ts">
   import { reactive, ref, computed, onMounted } from 'vue';
-  import { useRouter, useRoute } from 'vue-router';
+  import { useRouter } from 'vue-router';
   import { ElMessage } from 'element-plus';
   import { qualityMonitorScoreList } from '@/api/mark';
-  import { QMScoreItem, QMScoreListParam } from '@/api/types/mark';
+  import { QMScoreItem, MarkQualityMonitorListFilter } from '@/api/types/mark';
   import useLoading from '@/hooks/loading';
+  import { ls } from '@/utils/storage';
 
   defineOptions({
     name: 'ScoreCurve',
   });
 
   const router = useRouter();
-  const route = useRoute();
   const { loading, setLoading } = useLoading();
 
+  function getSearchModel() {
+    return ls.get('score-curve', {
+      subjectCode: '',
+      groupNumber: '',
+      marked: undefined,
+      noArbitrate: undefined,
+    }) as MarkQualityMonitorListFilter;
+  }
+
   // 从路由参数初始化筛选条件
-  const searchModel = reactive<QMScoreListParam>({
-    subjectCode: route.query.subjectCode
-      ? Number(route.query.subjectCode)
-      : null,
-    groupNumber: (route.query.groupNumber as string) || '',
-  });
+  const searchModel = reactive<MarkQualityMonitorListFilter>(getSearchModel());
 
   const dataList = ref<QMScoreItem[]>([]);