Sfoglia il codice sorgente

feat: 质量监控

zhangjie 2 giorni fa
parent
commit
23825e0465

+ 2 - 1
src/api/mark.ts

@@ -4,6 +4,7 @@ import {
   MarkQualityMonitorListPageParam,
   MarkQualityMonitorListPageRes,
   QMScoreItem,
+  QMScoreListParam,
   MarkArbitrationListPageRes,
   MarkArbitrationListPageParam,
   MarkTaskListPageRes,
@@ -35,7 +36,7 @@ export function qualityMonitorCalculate(
 }
 // 给分曲线
 export function qualityMonitorScoreList(
-  params: MarkQualityMonitorListFilter
+  params: QMScoreListParam
 ): Promise<QMScoreItem[]> {
   return axios.post('/api/mark/re-calculate', {}, { params });
 }

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

@@ -50,6 +50,12 @@ export interface QMScoreItem {
   // 给分
   scores: Array<{ score: number; count: number }>;
 }
+export type QMScoreListParam = {
+  // 科目
+  subject: number | null;
+  // 分组
+  group: string;
+};
 
 // 仲裁管理
 // 仲裁管理列表:科目代码	分组序号	准考证号	状态	创建时间	处理时间	处理人

+ 9 - 0
src/router/routes/modules/base.ts

@@ -207,6 +207,15 @@ const BASE: AppRouteRecordRaw = {
         requiresAuth: true,
       },
     },
+    {
+      path: '/quality-monitor/score-curve',
+      name: 'ScoreCurve',
+      component: () => import('@/views/mark/quality-monitor/ScoreCurve.vue'),
+      meta: {
+        title: '给分曲线',
+        requiresAuth: true,
+      },
+    },
   ],
 };
 

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

@@ -0,0 +1,140 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item label="分组">
+        <el-input
+          v-model.trim="searchModel.group"
+          placeholder="请输入分组"
+          clearable
+          style="width: 120px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-checkbox v-model="searchModel.marked"> 已评卷 </el-checkbox>
+        <el-checkbox v-model="searchModel.noArbitration">
+          不含仲裁
+        </el-checkbox>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button :loading="recalLoading" @click="onRecalculate"
+            >重新计算</el-button
+          >
+          <el-button @click="onViewScoreCurve">给分曲线</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column property="group" label="分组" width="100" />
+      <el-table-column property="markerId" label="评卷员" width="120" />
+      <el-table-column property="name" label="姓名" min-width="100" />
+      <el-table-column
+        property="completedTasks"
+        label="完成任务数"
+        width="120"
+      />
+      <el-table-column
+        property="arbitrationTasks"
+        label="仲裁任务数"
+        width="120"
+      />
+      <el-table-column label="仲裁率" width="100">
+        <template #default="scope">
+          {{ (scope.row.arbitrationRate * 100).toFixed(1) }}%
+        </template>
+      </el-table-column>
+      <el-table-column property="returnCount" label="打回次数" width="100" />
+      <el-table-column label="评卷采用率" width="120">
+        <template #default="scope">
+          {{ (scope.row.adoptionRate * 100).toFixed(1) }}%
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="markingSpeed"
+        label="评卷速度(秒)"
+        width="120"
+      />
+      <el-table-column property="averageScore" label="平均分" width="100">
+        <template #default="scope">
+          {{ scope.row.averageScore.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column property="standardDeviation" label="标准差" width="100">
+        <template #default="scope">
+          {{ scope.row.standardDeviation.toFixed(2) }}
+        </template>
+      </el-table-column>
+    </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 { useRouter } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+  import { getQualityMonitorList, qualityMonitorCalculate } from '@/api/mark';
+  import {
+    MarkQualityMonitorItem,
+    MarkQualityMonitorListFilter,
+  } from '@/api/types/mark';
+  import useTable from '@/hooks/table';
+  import useLoading from '@/hooks/loading';
+
+  defineOptions({
+    name: 'QualityMonitor',
+  });
+
+  const router = useRouter();
+
+  const searchModel = reactive<MarkQualityMonitorListFilter>({
+    subject: null,
+    group: '',
+    marked: undefined,
+    noArbitration: undefined,
+  });
+
+  const { dataList, pagination, loading, getList, toPage, pageSizeChange } =
+    useTable<MarkQualityMonitorItem>(getQualityMonitorList, searchModel, false);
+
+  // 重新计算
+  const { loading: recalLoading, setLoading: setrecalLoading } = useLoading();
+  async function onRecalculate() {
+    try {
+      setrecalLoading(true);
+      await qualityMonitorCalculate({ subject: searchModel.subject });
+      ElMessage.success('重新计算成功');
+      getList();
+    } catch (error) {
+      console.error('重新计算失败:', error);
+    } finally {
+      setrecalLoading(false);
+    }
+  }
+
+  function onViewScoreCurve() {
+    // 跳转到给分曲线页面,传递当前的筛选条件
+    router.push({
+      path: '/quality-monitor/score-curve',
+      query: {
+        subject: searchModel.subject?.toString() || '',
+        group: searchModel.group || '',
+      },
+    });
+  }
+</script>

+ 219 - 0
src/views/mark/quality-monitor/ScoreCurve.vue

@@ -0,0 +1,219 @@
+<template>
+  <div class="score-curve-page">
+    <!-- 页面头部 -->
+    <div class="page-header">
+      <h2 class="page-title">给分曲线</h2>
+      <el-button @click="goBack">返回</el-button>
+    </div>
+
+    <!-- 图表区域 -->
+    <div v-if="chartData.length > 0" class="part-box">
+      <h3>给分分布图</h3>
+      <Chart :options="chartOptions" height="400px" />
+    </div>
+
+    <!-- 数据表格 -->
+    <div class="part-box">
+      <el-table class="page-table" :data="dataList" :loading="loading">
+        <el-table-column property="marker" label="评卷员" width="120" />
+        <el-table-column property="name" label="姓名" min-width="100" />
+        <!-- 动态分数列 -->
+        <el-table-column
+          v-for="scoreRange in scoreRanges"
+          :key="scoreRange.label"
+          :label="scoreRange.label"
+          width="80"
+        >
+          <template #default="scope">
+            {{ getScoreCount(scope.row, scoreRange.score) }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref, computed, onMounted } from 'vue';
+  import { useRouter, useRoute } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+  import { qualityMonitorScoreList } from '@/api/mark';
+  import { QMScoreItem, QMScoreListParam } from '@/api/types/mark';
+  import useLoading from '@/hooks/loading';
+
+  defineOptions({
+    name: 'ScoreCurve',
+  });
+
+  const router = useRouter();
+  const route = useRoute();
+  const { loading, setLoading } = useLoading();
+
+  // 从路由参数初始化筛选条件
+  const searchModel = reactive<QMScoreListParam>({
+    subject: route.query.subject ? Number(route.query.subject) : null,
+    group: (route.query.group as string) || '',
+  });
+
+  const dataList = ref<QMScoreItem[]>([]);
+
+  // 计算所有可能的分数范围
+  const scoreRanges = computed(() => {
+    const ranges: Array<{ score: number; label: string }> = [];
+    const allScores = new Set<number>();
+
+    // 收集所有分数
+    dataList.value.forEach((item) => {
+      item.scores.forEach((scoreItem) => {
+        allScores.add(scoreItem.score);
+      });
+    });
+
+    // 排序并生成标签
+    const sortedScores = Array.from(allScores).sort((a, b) => a - b);
+    sortedScores.forEach((score) => {
+      ranges.push({
+        score,
+        label: `${score}分`,
+      });
+    });
+
+    return ranges;
+  });
+
+  // 获取指定分数的数量
+  const getScoreCount = (row: QMScoreItem, score: number): number => {
+    const scoreItem = row.scores.find((item) => item.score === score);
+    return scoreItem ? scoreItem.count : 0;
+  };
+
+  // 图表数据
+  const chartData = computed(() => {
+    if (dataList.value.length === 0) return [];
+
+    const seriesData: any[] = [];
+    const xAxisData = scoreRanges.value.map((range) => range.label);
+
+    dataList.value.forEach((item) => {
+      const data = scoreRanges.value.map((range) => {
+        const scoreItem = item.scores.find((s) => s.score === range.score);
+        return scoreItem ? scoreItem.count : 0;
+      });
+
+      seriesData.push({
+        name: `${item.name}`,
+        type: 'line',
+        data,
+        smooth: true,
+      });
+    });
+
+    return {
+      xAxisData,
+      seriesData,
+    };
+  });
+
+  // 图表配置
+  const chartOptions = computed(() => {
+    if (chartData.value.length === 0) return {};
+
+    return {
+      title: {
+        text: '给分曲线分布',
+        left: 'center',
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'cross',
+        },
+      },
+      legend: {
+        data: chartData.value.seriesData.map((item: any) => item.name),
+        top: 30,
+        type: 'scroll',
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '3%',
+        top: '15%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: chartData.value.xAxisData,
+        name: '分数',
+      },
+      yAxis: {
+        type: 'value',
+        name: '数量',
+      },
+      series: chartData.value.seriesData,
+    };
+  });
+
+  // 加载数据
+  async function loadData() {
+    if (!searchModel.subject) {
+      ElMessage.warning('请选择科目');
+      return;
+    }
+
+    try {
+      setLoading(true);
+      const result = await qualityMonitorScoreList(searchModel);
+      dataList.value = result;
+    } catch (error) {
+      console.error('加载给分曲线数据失败:', error);
+      ElMessage.error('加载数据失败');
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  // 返回上一页
+  function goBack() {
+    router.back();
+  }
+
+  onMounted(() => {
+    // 如果有默认科目,自动加载数据
+    if (searchModel.subject) {
+      loadData();
+    }
+  });
+</script>
+
+<style scoped lang="less">
+  .score-curve-page {
+    .page-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 16px;
+      padding: 16px 0;
+      border-bottom: 1px solid #e8e8e8;
+
+      .page-title {
+        margin: 0;
+        font-size: 20px;
+        font-weight: 500;
+        color: #333;
+      }
+    }
+
+    .part-box {
+      margin-bottom: 16px;
+
+      h3 {
+        margin: 0 0 16px 0;
+        font-size: 16px;
+        font-weight: 500;
+        color: #333;
+      }
+    }
+  }
+</style>