Sfoglia il codice sorgente

feat: 成绩复核

zhangjie 1 settimana fa
parent
commit
be7c0f74bd

+ 1 - 0
components.d.ts

@@ -9,6 +9,7 @@ export {};
 
 
 declare module '@vue/runtime-core' {
 declare module '@vue/runtime-core' {
   export interface GlobalComponents {
   export interface GlobalComponents {
+    Chart: typeof import('./src/components/chart/index.vue')['default'];
     ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'];
     ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'];
     ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'];
     ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'];
     ElButton: typeof import('element-plus/es')['ElButton'];
     ElButton: typeof import('element-plus/es')['ElButton'];

+ 1 - 0
config/vite.config.prod.ts

@@ -17,6 +17,7 @@ export default mergeConfig(
         output: {
         output: {
           manualChunks: {
           manualChunks: {
             arco: ['@arco-design/web-vue'],
             arco: ['@arco-design/web-vue'],
+            chart: ['echarts', 'vue-echarts'],
             vue: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
             vue: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
           },
           },
         },
         },

+ 2 - 0
package.json

@@ -33,6 +33,7 @@
     "axios": "^1.9.0",
     "axios": "^1.9.0",
     "crypto-js": "^4.2.0",
     "crypto-js": "^4.2.0",
     "dayjs": "^1.11.5",
     "dayjs": "^1.11.5",
+    "echarts": "^5.6.0",
     "element-plus": "^2.7.0",
     "element-plus": "^2.7.0",
     "js-md5": "^0.8.3",
     "js-md5": "^0.8.3",
     "lodash": "^4.17.21",
     "lodash": "^4.17.21",
@@ -42,6 +43,7 @@
     "pinia-plugin-persistedstate": "^3.2.1",
     "pinia-plugin-persistedstate": "^3.2.1",
     "query-string": "^8.0.3",
     "query-string": "^8.0.3",
     "vue": "^3.2.40",
     "vue": "^3.2.40",
+    "vue-echarts": "^7.0.3",
     "vue-ls": "^4.2.0",
     "vue-ls": "^4.2.0",
     "vue-router": "^4.0.14"
     "vue-router": "^4.0.14"
   },
   },

+ 53 - 0
pnpm-lock.yaml

@@ -24,6 +24,7 @@ specifiers:
   cross-env: ^7.0.3
   cross-env: ^7.0.3
   crypto-js: ^4.2.0
   crypto-js: ^4.2.0
   dayjs: ^1.11.5
   dayjs: ^1.11.5
+  echarts: ^5.6.0
   element-plus: ^2.7.0
   element-plus: ^2.7.0
   eslint: ^8.25.0
   eslint: ^8.25.0
   eslint-config-airbnb-base: ^15.0.0
   eslint-config-airbnb-base: ^15.0.0
@@ -56,6 +57,7 @@ specifiers:
   vite-plugin-imagemin: ^0.6.1
   vite-plugin-imagemin: ^0.6.1
   vite-svg-loader: ^3.6.0
   vite-svg-loader: ^3.6.0
   vue: ^3.2.40
   vue: ^3.2.40
+  vue-echarts: ^7.0.3
   vue-ls: ^4.2.0
   vue-ls: ^4.2.0
   vue-router: ^4.0.14
   vue-router: ^4.0.14
   vue-tsc: ^1.0.14
   vue-tsc: ^1.0.14
@@ -66,6 +68,7 @@ dependencies:
   axios: 1.9.0
   axios: 1.9.0
   crypto-js: 4.2.0
   crypto-js: 4.2.0
   dayjs: 1.11.13
   dayjs: 1.11.13
+  echarts: 5.6.0
   element-plus: 2.9.11_vue@3.5.16
   element-plus: 2.9.11_vue@3.5.16
   js-md5: 0.8.3
   js-md5: 0.8.3
   lodash: 4.17.21
   lodash: 4.17.21
@@ -75,6 +78,7 @@ dependencies:
   pinia-plugin-persistedstate: 3.2.3_pinia@2.3.1
   pinia-plugin-persistedstate: 3.2.3_pinia@2.3.1
   query-string: 8.2.0
   query-string: 8.2.0
   vue: 3.5.16_typescript@4.9.5
   vue: 3.5.16_typescript@4.9.5
+  vue-echarts: 7.0.3_echarts@5.6.0+vue@3.5.16
   vue-ls: 4.2.0
   vue-ls: 4.2.0
   vue-router: 4.5.1_vue@3.5.16
   vue-router: 4.5.1_vue@3.5.16
 
 
@@ -2257,6 +2261,13 @@ packages:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
     dev: true
     dev: true
 
 
+  /echarts/5.6.0:
+    resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
+    dependencies:
+      tslib: 2.3.0
+      zrender: 5.6.1
+    dev: false
+
   /element-plus/2.9.11_vue@3.5.16:
   /element-plus/2.9.11_vue@3.5.16:
     resolution: {integrity: sha512-x4L/6YC8de4JtuE3vpaEugJdQIeHQaHtIYKyk67IeF6dTIiVax45aX4nWOygnh+xX+0gTvL6xO+9BZhPA3G82w==}
     resolution: {integrity: sha512-x4L/6YC8de4JtuE3vpaEugJdQIeHQaHtIYKyk67IeF6dTIiVax45aX4nWOygnh+xX+0gTvL6xO+9BZhPA3G82w==}
     peerDependencies:
     peerDependencies:
@@ -6484,6 +6495,10 @@ packages:
     resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
     resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
     dev: true
     dev: true
 
 
+  /tslib/2.3.0:
+    resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
+    dev: false
+
   /tslib/2.8.1:
   /tslib/2.8.1:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
     dev: true
     dev: true
@@ -6887,6 +6902,21 @@ packages:
       fsevents: 2.3.3
       fsevents: 2.3.3
     dev: true
     dev: true
 
 
+  /vue-demi/0.13.11_vue@3.5.16:
+    resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
+    engines: {node: '>=12'}
+    hasBin: true
+    requiresBuild: true
+    peerDependencies:
+      '@vue/composition-api': ^1.0.0-rc.1
+      vue: ^3.0.0-0 || ^2.6.0
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+    dependencies:
+      vue: 3.5.16_typescript@4.9.5
+    dev: false
+
   /vue-demi/0.14.10_vue@3.5.16:
   /vue-demi/0.14.10_vue@3.5.16:
     resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
     resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
@@ -6901,6 +6931,23 @@ packages:
     dependencies:
     dependencies:
       vue: 3.5.16_typescript@4.9.5
       vue: 3.5.16_typescript@4.9.5
 
 
+  /vue-echarts/7.0.3_echarts@5.6.0+vue@3.5.16:
+    resolution: {integrity: sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==}
+    peerDependencies:
+      '@vue/runtime-core': ^3.0.0
+      echarts: ^5.5.1
+      vue: ^2.7.0 || ^3.1.1
+    peerDependenciesMeta:
+      '@vue/runtime-core':
+        optional: true
+    dependencies:
+      echarts: 5.6.0
+      vue: 3.5.16_typescript@4.9.5
+      vue-demi: 0.13.11_vue@3.5.16
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+    dev: false
+
   /vue-eslint-parser/9.4.3_eslint@8.57.1:
   /vue-eslint-parser/9.4.3_eslint@8.57.1:
     resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
     resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
     engines: {node: ^14.17.0 || >=16.0.0}
     engines: {node: ^14.17.0 || >=16.0.0}
@@ -7144,3 +7191,9 @@ packages:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
     engines: {node: '>=10'}
     dev: true
     dev: true
+
+  /zrender/5.6.1:
+    resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
+    dependencies:
+      tslib: 2.3.0
+    dev: false

+ 34 - 0
src/api/review.ts

@@ -0,0 +1,34 @@
+import axios from 'axios';
+import {
+  ReviewStatInfo,
+  ReviewStatListPageParams,
+  ReviewStatListPageRes,
+  FullReviewListPageParams,
+  FullReviewListPageRes,
+  ScoreCheckListPageRes,
+} from './types/review';
+
+// 复核管理
+// 获取复核进度统计信息
+export function getReviewStatInfo(): Promise<ReviewStatInfo> {
+  return axios.get('/api/review/stat/info');
+}
+
+// 获取复核进度列表信息
+export function getReviewStatList(
+  params: ReviewStatListPageParams
+): Promise<ReviewStatListPageRes> {
+  return axios.post('/api/review/stat/list', { params });
+}
+
+// 获取全卷复核列表信息
+export function getFullReviewList(
+  params: FullReviewListPageParams
+): Promise<FullReviewListPageRes> {
+  return axios.post('/api/review/full/list', { params });
+}
+
+// 获取成绩校验列表信息
+export function getScoreCheckList(): Promise<ScoreCheckListPageRes> {
+  return axios.post('/api/review/score/check/list', {});
+}

+ 118 - 0
src/api/types/review.ts

@@ -0,0 +1,118 @@
+import { OptionalExceptionType } from '@/constants/enumerate';
+import { PageResult, PageParams } from './common';
+
+export interface ReviewStatInfo {
+  // 任务已完成数
+  taskFinishedCount: number;
+  // 任务待完成数
+  taskUnfinishedCount: number;
+  // 科目已完成数
+  courseFinishedCount: number;
+  // 科目待完成数
+  courseUnfinishedCount: number;
+}
+
+// 复核统计列表项目: 科目名称 选做科目 试卷总量	任务数	已复核数	待复核数	完成进度	已复核次数
+export interface ReviewStatItem {
+  // 科目名称
+  courseName: string;
+  // 选做科目
+  isOptional: boolean;
+  // 试卷总量
+  paperCount: number;
+  // 任务数
+  taskCount: number;
+  // 已复核数
+  reviewedCount: number;
+  // 待复核数
+  unReviewedCount: number;
+  // 完成进度
+  progress: number;
+  // 已复核次数
+  reviewedTimes: number;
+}
+export type ReviewStatListPageRes = PageResult<ReviewStatItem>;
+
+export interface ReviewStatListFilter {
+  // 科目
+  subjectId: number | null;
+  // 选做科目
+  isOptional: boolean | null;
+  // 完成进度
+  isFinished: boolean | null;
+}
+export type ReviewStatListPageParams = PageParams<ReviewStatListFilter>;
+
+// 全卷复核
+// 全卷复核列表项目:科目	密号	选做异常	客观分	主观分	试卷总分	得分明细
+export interface FullReviewItem {
+  // 主键
+  id: number;
+  // 科目
+  courseName: string;
+  // 密号
+  secretNo: string;
+  // 选做异常
+  isOptionalException: boolean;
+  // 客观分
+  objectiveScore: number;
+  // 主观分
+  subjectiveScore: number;
+  // 试卷总分
+  totalScore: number;
+  // 得分明细
+  scoreDetail: string;
+}
+export type FullReviewListPageRes = PageResult<FullReviewItem>;
+
+export interface FullReviewListFilter {
+  // 科目
+  subjectId?: number;
+  // 选做异常
+  isOptionalException?: boolean;
+  // 状态:已复核、未复核
+  isReviewed?: boolean;
+  // 学院
+  academyId?: number;
+  // 试卷总分
+  totalStartScore?: number;
+  totalEndScore?: number;
+  // 选做题
+  optionalExceptionType?: OptionalExceptionType;
+  // 复核次数
+  reviewedTimes?: number;
+  // 复核人
+  reviewer?: string;
+  // 大题号
+  bigQuestionNo?: number;
+  // 大题得分
+  bigQuestionStartScore?: number;
+  bigQuestionEndScore?: number;
+  // 小题得分
+  smallQuestionScore?: number;
+  // 密号
+  secretNo?: string;
+}
+export type FullReviewListPageParams = PageParams<FullReviewListFilter>;
+
+// 成绩校验
+// 成绩校验列表项目:考生编号 科目 满分 状态 试卷总分 得分明细
+export interface ScoreCheckItem {
+  // 主键
+  id: number;
+  // 考生编号
+  studentNo: string;
+  // 科目
+  courseName: string;
+  // 科目编号
+  courseCode: string;
+  // 满分
+  totalScore: number;
+  // 状态
+  status: string;
+  // 试卷总分
+  paperScore: number;
+  // 得分明细
+  scoreDetail: string;
+}
+export type ScoreCheckListPageRes = PageResult<ScoreCheckItem>;

+ 4 - 0
src/assets/style/base.less

@@ -22,6 +22,10 @@
   }
   }
   &.is-filter {
   &.is-filter {
     padding-bottom: 4px;
     padding-bottom: 4px;
+
+    .el-form + .el-space {
+      padding-bottom: 10px;
+    }
   }
   }
 
 
   &-head {
   &-head {

+ 41 - 0
src/components/chart/index.vue

@@ -0,0 +1,41 @@
+<template>
+  <VCharts
+    v-if="renderChart"
+    :option="options"
+    :autoresize="autoResize"
+    :style="{ width, height }"
+  />
+</template>
+
+<script lang="ts" setup>
+  import { ref, nextTick } from 'vue';
+  import VCharts from 'vue-echarts';
+
+  defineProps({
+    options: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    autoResize: {
+      type: Boolean,
+      default: true,
+    },
+    width: {
+      type: String,
+      default: '100%',
+    },
+    height: {
+      type: String,
+      default: '100%',
+    },
+  });
+  const renderChart = ref(false);
+  // wait container expand
+  nextTick(() => {
+    renderChart.value = true;
+  });
+</script>
+
+<style scoped lang="less"></style>

+ 4 - 0
src/components/index.ts

@@ -1,4 +1,6 @@
 import { App } from 'vue';
 import { App } from 'vue';
+import '../utils/echarts';
+import Chart from './chart/index.vue';
 
 
 // selection
 // selection
 import SvgIcon from './svg-icon/index.vue';
 import SvgIcon from './svg-icon/index.vue';
@@ -11,6 +13,8 @@ import ImportDialog from './import-dialog/index.vue';
 
 
 export default {
 export default {
   install(Vue: App) {
   install(Vue: App) {
+    Vue.component('Chart', Chart);
+
     Vue.component('SvgIcon', SvgIcon);
     Vue.component('SvgIcon', SvgIcon);
     Vue.component('SelectRangeDatetime', SelectRangeDatetime);
     Vue.component('SelectRangeDatetime', SelectRangeDatetime);
     Vue.component('SelectRangeTime', SelectRangeTime);
     Vue.component('SelectRangeTime', SelectRangeTime);

+ 8 - 0
src/constants/enumerate.ts

@@ -69,3 +69,11 @@ export const NUMBER_SCOPE_MODE = {
   EQUAL: '等于',
   EQUAL: '等于',
 };
 };
 export type NumberScopeMode = keyof typeof NUMBER_SCOPE_MODE;
 export type NumberScopeMode = keyof typeof NUMBER_SCOPE_MODE;
+
+// 选做题异常类型:未选做 多选做 少选做
+export const OPTIONAL_EXCEPTION_TYPE = {
+  NOT_SELECTED: '未选做',
+  MULTI_SELECTED: '多选做',
+  LESS_SELECTED: '少选做',
+};
+export type OptionalExceptionType = keyof typeof OPTIONAL_EXCEPTION_TYPE;

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

@@ -144,6 +144,33 @@ const BASE: AppRouteRecordRaw = {
         requiresAuth: true,
         requiresAuth: true,
       },
       },
     },
     },
+    {
+      path: '/score-review-statistics',
+      name: 'ScoreReviewStatistics',
+      component: () => import('@/views/review/ScoreReviewStatistics.vue'),
+      meta: {
+        title: '复核进度统计',
+        requiresAuth: true,
+      },
+    },
+    {
+      path: '/all-review',
+      name: 'AllReview',
+      component: () => import('@/views/review/AllReview.vue'),
+      meta: {
+        title: '全卷复核',
+        requiresAuth: true,
+      },
+    },
+    {
+      path: '/score-check',
+      name: 'ScoreCheck',
+      component: () => import('@/views/review/ScoreQuery.vue'),
+      meta: {
+        title: '成绩校验',
+        requiresAuth: true,
+      },
+    },
   ],
   ],
 };
 };
 
 

+ 24 - 0
src/utils/echarts.ts

@@ -0,0 +1,24 @@
+import { use } from 'echarts/core';
+import { CanvasRenderer } from 'echarts/renderers';
+import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
+import {
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  DataZoomComponent,
+  GraphicComponent,
+} from 'echarts/components';
+
+// Manually introduce ECharts modules to reduce packing size
+use([
+  CanvasRenderer,
+  BarChart,
+  LineChart,
+  PieChart,
+  RadarChart,
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  DataZoomComponent,
+  GraphicComponent,
+]);

+ 316 - 0
src/views/review/AllReview.vue

@@ -0,0 +1,316 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subjectId"></select-subject>
+      </el-form-item>
+      <el-form-item label="选做科目">
+        <el-select
+          v-model="searchModel.isOptionalException"
+          placeholder="不限"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="是" :value="true" />
+          <el-option label="否" :value="false" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-select
+          v-model="searchModel.isReviewed"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="已复核" :value="true" />
+          <el-option label="未复核" :value="false" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="学院">
+        <el-select
+          v-model="searchModel.academyId"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="请选11择" :value="11" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="试卷总分">
+        <el-space>
+          <el-input-number
+            v-model="searchModel.totalStartScore"
+            placeholder="低分"
+            :min="0"
+            :max="999"
+            :step="0.1"
+            :precision="1"
+            :controls="false"
+            step-strictly
+            style="width: 80px"
+          />
+          <span>到</span>
+          <el-input-number
+            v-model="searchModel.totalEndScore"
+            placeholder="高分"
+            :min="0"
+            :max="999"
+            :step="0.1"
+            :precision="1"
+            :controls="false"
+            step-strictly
+            style="width: 80px"
+          />
+        </el-space>
+      </el-form-item>
+      <el-form-item label="选做题">
+        <el-select
+          v-model="searchModel.optionalExceptionType"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option
+            v-for="(val, key) in OPTIONAL_EXCEPTION_TYPE"
+            :key="key"
+            :label="val"
+            :value="key"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="复核次数">
+        <el-select
+          v-model="searchModel.reviewedTimes"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="1" :value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="复核人">
+        <el-input
+          v-model.trim="searchModel.reviewer"
+          placeholder=""
+          clearable
+          style="width: 120px"
+        />
+      </el-form-item>
+      <el-form-item label="大题">
+        <el-select
+          v-model="searchModel.bigQuestionNo"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="请选择" :value="1" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="大题得分">
+        <el-space>
+          <el-input-number
+            v-model="searchModel.bigQuestionStartScore"
+            placeholder="低分"
+            :min="0"
+            :max="999"
+            :step="0.1"
+            :precision="1"
+            :controls="false"
+            step-strictly
+            style="width: 80px"
+          />
+          <span>到</span>
+          <el-input-number
+            v-model="searchModel.bigQuestionEndScore"
+            placeholder="高分"
+            :min="0"
+            :max="999"
+            :step="0.1"
+            :precision="1"
+            :controls="false"
+            step-strictly
+            style="width: 80px"
+          />
+        </el-space>
+      </el-form-item>
+      <el-form-item label="小题得分">
+        <el-input-number
+          v-model="searchModel.smallQuestionScore"
+          placeholder=""
+          :min="0"
+          :max="999"
+          :step="0.1"
+          :precision="1"
+          :controls="false"
+          step-strictly
+          style="width: 120px"
+        />
+      </el-form-item>
+      <el-form-item label="密号">
+        <el-input
+          v-model.trim="searchModel.secretNo"
+          placeholder=""
+          clearable
+          style="width: 120px"
+        />
+      </el-form-item>
+    </el-form>
+    <el-space wrap>
+      <el-button type="primary" @click="toPage(1)">查询</el-button>
+      <el-button @click="onBatchReview"
+        >批量复核({{ pagination.total }})</el-button
+      >
+      <el-button @click="onBatchCancelReview">取消复核</el-button>
+      <el-button @click="onImport">导入</el-button>
+      <el-dropdown @command="onExportCommand">
+        <el-button type="primary">
+          导出
+          <el-icon class="el-icon--right"><ArrowDown /> </el-icon>
+        </el-button>
+        <template #dropdown>
+          <el-dropdown-menu>
+            <el-dropdown-item command="list">复核列表</el-dropdown-item>
+            <el-dropdown-item command="gzl">工作量</el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+      <el-button @click="onExportSign">导出标记卷</el-button>
+      <el-button @click="onExportLog">错误日志</el-button>
+    </el-space>
+  </div>
+
+  <div class="part-box">
+    <el-table
+      class="page-table"
+      :data="dataList"
+      :loading="loading"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" />
+      <el-table-column property="courseName" label="科目" width="150" />
+      <el-table-column property="secretNo" label="密号" width="120" />
+      <el-table-column label="选做异常" width="100">
+        <template #default="scope">
+          <el-tag :type="scope.row.isOptionalException ? 'warning' : 'success'">
+            {{ scope.row.isOptionalException ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column property="objectiveScore" label="客观分" width="100" />
+      <el-table-column property="subjectiveScore" label="主观分" width="100" />
+      <el-table-column property="totalScore" label="试卷总分" width="100" />
+      <el-table-column
+        property="scoreDetail"
+        label="得分明细"
+        min-width="200"
+      />
+      <el-table-column label="操作" width="100" fixed="right">
+        <template #default="scope">
+          <el-button size="small" link @click="onTrackView(scope.row)">
+            轨迹图
+          </el-button>
+          <el-button size="small" link @click="onCancelReview(scope.row)">
+            取消复核
+          </el-button>
+          <el-button size="small" link @click="onReview(scope.row)">
+            进入复核
+          </el-button>
+        </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, ref } from 'vue';
+  import { ArrowDown } from '@element-plus/icons-vue';
+  import { getFullReviewList } from '@/api/review';
+  import { FullReviewItem, FullReviewListFilter } from '@/api/types/review';
+  import useTable from '@/hooks/table';
+
+  import { OPTIONAL_EXCEPTION_TYPE } from '@/constants/enumerate';
+
+  defineOptions({
+    name: 'AllReview',
+  });
+
+  const searchModel = reactive<FullReviewListFilter>({
+    subjectId: null,
+    isOptionalException: undefined,
+    isReviewed: undefined,
+    academyId: undefined,
+    totalStartScore: undefined,
+    totalEndScore: undefined,
+    optionalExceptionType: undefined,
+    reviewedTimes: undefined,
+    reviewer: undefined,
+    bigQuestionNo: undefined,
+    bigQuestionStartScore: undefined,
+    bigQuestionEndScore: undefined,
+    smallQuestionScore: undefined,
+    secretNo: undefined,
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<FullReviewItem>(getFullReviewList, searchModel, false);
+
+  const selectedRows = ref<FullReviewItem[]>([]);
+
+  const handleSelectionChange = (selection: FullReviewItem[]) => {
+    selectedRows.value = selection;
+  };
+
+  // 批量复核
+  function onBatchReview() {
+    // TODO: 批量复核
+  }
+  // 批量取消复核
+  function onBatchCancelReview() {
+    // TODO: 批量取消复核
+  }
+
+  // 轨迹图
+  function onTrackView(row: FullReviewItem) {
+    // TODO: 轨迹图
+    console.log('复核:', row);
+  }
+  // 取消复核
+  function onCancelReview(row: FullReviewItem) {
+    // TODO: 取消复核
+    console.log('复核:', row);
+  }
+  // 进入复核
+  function onReview(row: FullReviewItem) {
+    // TODO: 进入复核
+    console.log('复核:', row);
+  }
+
+  // 导入
+  function onImport() {
+    // TODO: 实现导入功能
+    console.log('导入');
+  }
+
+  // 导出
+  const onExportCommand = (command: string) => {
+    console.log('导出命令:', command);
+  };
+  // 导出标记卷
+  function onExportSign() {
+    // TODO: 导出标记卷
+    console.log('导出标记卷');
+  }
+  // 导出错误日志
+  function onExportLog() {
+    // TODO: 导出错误日志
+    console.log('导出错误日志');
+  }
+</script>

+ 73 - 0
src/views/review/ScoreQuery.vue

@@ -0,0 +1,73 @@
+<template>
+  <div class="part-box">
+    <el-space wrap>
+      <el-button>查重</el-button>
+      <el-button>批量校验:{{ dataList.length }}</el-button>
+      <el-button @click="onExport">导出</el-button>
+    </el-space>
+  </div>
+
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="selection" width="55" />
+      <el-table-column property="studentNo" label="考生编号" width="150" />
+      <el-table-column property="courseName" label="科目" width="200" />
+      <el-table-column property="totalScore" label="满分" width="100" />
+      <el-table-column label="状态" width="120">
+        <template #default="scope">
+          <el-tag :type="getStatusType(scope.row.status)">
+            {{ scope.row.status }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column property="paperScore" label="试卷总分" width="120" />
+      <el-table-column property="scoreDetail" label="得分明细" min-width="300">
+      </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 { getScoreCheckList } from '@/api/review';
+  import { ScoreCheckItem } from '@/api/types/review';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'ScoreQuery',
+  });
+
+  // 由于接口不需要参数,使用空对象
+  const searchModel = reactive({});
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<ScoreCheckItem>(getScoreCheckList, searchModel, false);
+
+  // 获取状态标签类型
+  function getStatusType(status: string) {
+    switch (status) {
+      case '已上传':
+        return 'success';
+      case '正常':
+        return 'success';
+      case '未上传':
+        return 'warning';
+      default:
+        return 'info';
+    }
+  }
+
+  // 导出
+  function onExport() {
+    // TODO: 实现导出功能
+    console.log('导出');
+  }
+</script>

+ 238 - 0
src/views/review/ScoreReviewStatistics.vue

@@ -0,0 +1,238 @@
+<template>
+  <div class="part-box">
+    <!-- 统计图表区域 -->
+    <div class="chart-container">
+      <div class="chart-item">
+        <h3>复核总进度</h3>
+        <Chart :options="taskChartOptions" width="400px" height="300px" />
+      </div>
+      <div class="chart-item">
+        <h3>科目复核进度</h3>
+        <Chart :options="courseChartOptions" width="400px" height="300px" />
+      </div>
+    </div>
+  </div>
+
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subjectId"></select-subject>
+      </el-form-item>
+      <el-form-item label="选做科目">
+        <el-select
+          v-model="searchModel.isOptional"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="是" :value="true" />
+          <el-option label="否" :value="false" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="完成进度">
+        <el-select
+          v-model="searchModel.isFinished"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="已完成" :value="true" />
+          <el-option label="未完成" :value="false" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="toPage(1)">查询</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column property="courseName" label="科目" min-width="200" />
+      <el-table-column label="选做科目" min-width="100">
+        <template #default="scope">
+          <el-tag :type="scope.row.isOptional ? 'success' : 'info'">
+            {{ scope.row.isOptional ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column property="paperCount" label="试卷总量" min-width="100" />
+      <el-table-column property="taskCount" label="任务数" min-width="80" />
+      <el-table-column property="reviewedCount" label="已复核数" width="100" />
+      <el-table-column
+        property="unReviewedCount"
+        label="待复核数"
+        min-width="100"
+      />
+      <el-table-column label="完成进度" min-width="100">
+        <template #default="scope">
+          <el-progress
+            :percentage="scope.row.progress"
+            :color="scope.row.progress === 100 ? '#67c23a' : '#409eff'"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="reviewedTimes"
+        label="已复核次数"
+        min-width="100"
+      />
+    </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, ref, computed, onMounted } from 'vue';
+  import { getReviewStatInfo, getReviewStatList } from '@/api/review';
+  import {
+    ReviewStatInfo,
+    ReviewStatItem,
+    ReviewStatListFilter,
+  } from '@/api/types/review';
+  import useTable from '@/hooks/table';
+  import Chart from '@/components/chart/index.vue';
+
+  defineOptions({
+    name: 'ScoreReviewStatistics',
+  });
+
+  const searchModel = reactive<ReviewStatListFilter>({
+    subjectId: null,
+    isOptional: null,
+    isFinished: null,
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<ReviewStatItem>(getReviewStatList, searchModel, false);
+
+  // 统计信息
+  const statInfo = ref<ReviewStatInfo>({
+    taskFinishedCount: 0,
+    taskUnfinishedCount: 0,
+    courseFinishedCount: 0,
+    courseUnfinishedCount: 0,
+  });
+
+  // 任务进度饼状图配置
+  const taskChartOptions = computed(() => ({
+    tooltip: {
+      trigger: 'item',
+      formatter: '{a} <br/>{b}: {c} ({d}%)',
+    },
+    legend: {
+      orient: 'horizontal',
+      bottom: '10%',
+      data: ['任务待完成', '任务已完成'],
+    },
+    series: [
+      {
+        name: '任务进度',
+        type: 'pie',
+        radius: ['40%', '70%'],
+        center: ['50%', '40%'],
+        data: [
+          {
+            value: statInfo.value.taskUnfinishedCount,
+            name: '任务待完成',
+            itemStyle: { color: '#5470c6' },
+          },
+          {
+            value: statInfo.value.taskFinishedCount,
+            name: '任务已完成',
+            itemStyle: { color: '#ff7875' },
+          },
+        ],
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)',
+          },
+        },
+      },
+    ],
+  }));
+
+  // 科目进度饼状图配置
+  const courseChartOptions = computed(() => ({
+    tooltip: {
+      trigger: 'item',
+      formatter: '{a} <br/>{b}: {c} ({d}%)',
+    },
+    legend: {
+      orient: 'horizontal',
+      bottom: '10%',
+      data: ['科目待完成', '科目已完成'],
+    },
+    series: [
+      {
+        name: '科目进度',
+        type: 'pie',
+        radius: ['40%', '70%'],
+        center: ['50%', '40%'],
+        data: [
+          {
+            value: statInfo.value.courseUnfinishedCount,
+            name: '科目待完成',
+            itemStyle: { color: '#5470c6' },
+          },
+          {
+            value: statInfo.value.courseFinishedCount,
+            name: '科目已完成',
+            itemStyle: { color: '#ff7875' },
+          },
+        ],
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)',
+          },
+        },
+      },
+    ],
+  }));
+
+  // 获取统计信息
+  async function getStatInfo() {
+    try {
+      const data = await getReviewStatInfo();
+      statInfo.value = data;
+    } catch (error) {
+      console.error('获取统计信息失败:', error);
+    }
+  }
+
+  onMounted(() => {
+    getStatInfo();
+  });
+</script>
+
+<style scoped lang="less">
+  .chart-container {
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+    padding: 20px;
+    gap: 40px;
+
+    .chart-item {
+      text-align: center;
+
+      h3 {
+        margin-bottom: 20px;
+        color: #333;
+        font-size: 16px;
+        font-weight: 500;
+      }
+    }
+  }
+</style>