瀏覽代碼

feat: 考试管理

zhangjie 1 周之前
父節點
當前提交
f21f4453d4

+ 2 - 0
components.d.ts

@@ -13,6 +13,7 @@ declare module '@vue/runtime-core' {
     ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'];
     ElButton: typeof import('element-plus/es')['ElButton'];
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox'];
+    ElCol: typeof import('element-plus/es')['ElCol'];
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'];
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker'];
     ElDescriptions: typeof import('element-plus/es')['ElDescriptions'];
@@ -33,6 +34,7 @@ declare module '@vue/runtime-core' {
     ElPagination: typeof import('element-plus/es')['ElPagination'];
     ElProgress: typeof import('element-plus/es')['ElProgress'];
     ElResult: typeof import('element-plus/es')['ElResult'];
+    ElRow: typeof import('element-plus/es')['ElRow'];
     ElSelect: typeof import('element-plus/es')['ElSelect'];
     ElSpace: typeof import('element-plus/es')['ElSpace'];
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu'];

+ 1 - 0
src/api/base.ts

@@ -1,5 +1,6 @@
 import axios from 'axios';
 import type { ExamItem, SubjectItem } from './types/base';
+// import type { ExamQueryItem } from './types/exam';
 
 // 通用查询
 // 通用查询-考试

+ 31 - 0
src/api/exam.ts

@@ -0,0 +1,31 @@
+import axios from 'axios';
+import {
+  ExamListPageParam,
+  ExamListPageRes,
+  ExamItem,
+  ExamUpdateParam,
+  ExamStatDetailInfo,
+} from './types/exam';
+
+// 考试列表
+export function getExamList(
+  params: ExamListPageParam
+): Promise<ExamListPageRes> {
+  return axios.post('/api/exam/list', { params });
+}
+// 考试详情
+export function getExamDetail(examId: number): Promise<ExamItem> {
+  return axios.post('/api/exam/detail', { examId });
+}
+
+// 新增或编辑考试
+export function updateExam(data: ExamUpdateParam): Promise<ExamItem> {
+  return axios.post('/api/exam/update', data);
+}
+
+// 获取考试统计详情信息
+export function getExamStatisticInfo(
+  examId: number
+): Promise<ExamStatDetailInfo> {
+  return axios.post('/api/exam/statistic', {}, { params: { examId } });
+}

+ 16 - 0
src/api/issue-paper.ts

@@ -2,6 +2,8 @@ import axios from 'axios';
 import {
   IssuePaperListPageRes,
   IssuePaperListPageParam,
+  IssuePaperTypeItem,
+  IssuePaperTypeUpdateParam,
 } from './types/issue-paper';
 
 // 获取问题卷列表
@@ -15,3 +17,17 @@ export function getIssuePaperList(
 export function resetIssuePaper(ids: number[]): Promise<any> {
   return axios.post('/api/score/reset', { ids });
 }
+
+// 查询问题卷类型
+export function getIssuePaperTypeList(
+  examId: number
+): Promise<IssuePaperTypeItem[]> {
+  return axios.post('/api/score/type/list', {}, { params: { examId } });
+}
+
+// 新增/修改问题卷类型
+export function addIssuePaperType(
+  params: IssuePaperTypeUpdateParam
+): Promise<any> {
+  return axios.post('/api/score/type/add', params);
+}

+ 11 - 0
src/api/reject.ts

@@ -4,6 +4,8 @@ import {
   RejectListPageRes,
   RejectStatisticsListParams,
   RejectStatisticsListRes,
+  RejectTypeItem,
+  RejectTypeUpdateParam,
 } from './types/reject';
 
 // 获取打回列表
@@ -25,3 +27,12 @@ export function getRejectStatisticsList(
 ): Promise<RejectStatisticsListRes> {
   return axios.post('/api/student/list', { params });
 }
+
+// 查询打回类型
+export function getRejectTypeList(examId: number): Promise<RejectTypeItem[]> {
+  return axios.post('/api/student/list', {}, { params: { examId } });
+}
+// 新增/修改打回类型
+export function updateRejectType(params: RejectTypeUpdateParam): Promise<any> {
+  return axios.post('/api/student/list', params);
+}

+ 112 - 0
src/api/types/exam.ts

@@ -0,0 +1,112 @@
+import { PageResult, PageParams } from './common';
+
+export interface ExamBaseItem {
+  id: number;
+  // 考试名称
+  name: string;
+  // 类型
+  type: string;
+  // 扫描图片类型
+  scanImageType: number;
+  // 考试日期
+  examDate: string;
+  // 评卷开始日期
+  markingStartDate: string;
+  // 评卷结束日期
+  markingEndDate: string;
+  // 评卷模式
+  markingMode: 'TRACK' | 'COMMON' | 'NONE';
+  // 强制标记
+  forceMark: boolean;
+  // 禁止其他人查看考生信息
+  forbidViewStudentInfo: boolean;
+  // 禁止科组长成绩查询
+  forbidLeaderScoreQuery: boolean;
+  // 全卷多次复核不能同一账号
+  preventSameAccountRecheck: boolean;
+  // 全卷复核强制试卷拉到底部
+  forceScrollToBottom: boolean;
+  // 全卷复核进度100%时才能再次复核
+  requireFullProgressForRecheck: boolean;
+  // 评卷端是否显示客观分
+  showObjectiveScore: boolean;
+  // 打回后显示原分值
+  showOriginalScoreAfterReturn: boolean;
+  // 回评卷数
+  recheckCount: number;
+  // 及格分
+  passingScore: number;
+  // 优秀分
+  excellentScore: number;
+  // 原卷显示
+  showOriginalPaper: boolean;
+  // 评卷提交自动定位
+  autoPositionAfterSubmit: boolean;
+  // 自动对切题卡
+  autoAlignAnswerSheet: boolean;
+  // 描述
+  description: string;
+  // 状态
+  status: string;
+  // 创建时间
+  createTime: string;
+}
+
+// 高级配置
+export interface ExamAdvancedConfig {
+  // 评卷时强制试卷拉到底部
+  forceScrollToBottom: boolean;
+  // 最小阅卷时长(秒)
+  minMarkingDuration: number | undefined;
+  // 单卷回评次数
+  singlePaperRecheckCount: number | undefined;
+  // 给分次数限制类型:等于 小于等于
+  scoringLimitType: 'eq' | 'le' | null;
+  // 启用条码粘贴AI检测
+  enableBarcodeAIDetection: boolean;
+  // 启用题卡作答AI检测
+  enableAnswerSheetAIDetection: boolean;
+}
+
+export type ExamItem = ExamBaseItem & ExamAdvancedConfig;
+
+export type ExamQueryItem = Pick<
+  ExamBaseItem,
+  | 'id'
+  | 'name'
+  | 'type'
+  | 'examDate'
+  | 'markingStartDate'
+  | 'markingEndDate'
+  | 'status'
+  | 'forceMark'
+>;
+
+export type ExamListPageRes = PageResult<ExamQueryItem>;
+
+export interface ExamListFilter {
+  // 考试名称
+  name: string;
+  // 类型
+  type: string;
+  // 状态
+  status: string;
+}
+export type ExamListPageParam = PageParams<ExamListFilter>;
+
+// 基础配置
+export type ExamUpdateParam = Omit<ExamItem, 'id' | 'createTime'>;
+
+// 统计详情
+export interface ExamStatDetailInfo {
+  // 考生数量
+  studentCount: number;
+  // 科目数量
+  subjectCount: number;
+  // 扫描进度
+  scanProgress: number;
+  // 扫描张数量
+  scanImageCount: number;
+  // 评卷进度
+  markingProgress: number;
+}

+ 16 - 0
src/api/types/issue-paper.ts

@@ -43,3 +43,19 @@ export interface IssuePaperListFilter {
 }
 
 export type IssuePaperListPageParam = PageParams<IssuePaperListFilter>;
+
+// 问题卷分类
+export interface IssuePaperTypeItem {
+  // 编号
+  id: number;
+  // 名称
+  name: string;
+  // 类型
+  type: string;
+}
+
+export interface IssuePaperTypeUpdateParam {
+  id?: number;
+  examId: number;
+  name: string;
+}

+ 16 - 0
src/api/types/reject.ts

@@ -61,3 +61,19 @@ export interface RejectStatisticsFilter {
   showGroupNo: boolean;
 }
 export type RejectStatisticsListParams = PageParams<RejectStatisticsFilter>;
+
+// 打回卷分类
+export interface RejectTypeItem {
+  // 编号
+  id: number;
+  // 名称
+  name: string;
+  // 类型
+  type: string;
+}
+
+export interface RejectTypeUpdateParam {
+  id?: number;
+  examId: number;
+  name: string;
+}

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

@@ -36,6 +36,24 @@ const BASE: AppRouteRecordRaw = {
         requiresAuth: true,
       },
     },
+    {
+      path: '/exam-manage',
+      name: 'ExamManage',
+      component: () => import('@/views/exam/ExamManage.vue'),
+      meta: {
+        title: '考试管理',
+        requiresAuth: true,
+      },
+    },
+    {
+      path: '/exam-edit',
+      name: 'ExamEdit',
+      component: () => import('@/views/exam/ExamEdit.vue'),
+      meta: {
+        title: '编辑考试',
+        requiresAuth: true,
+      },
+    },
     {
       path: '/reject-manage',
       name: 'RejectManage',

+ 222 - 0
src/views/exam/ExamAdvancedDialog.vue

@@ -0,0 +1,222 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="高级设置"
+    width="700px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    top="10vh"
+    append-to-body
+    @close="handleClose"
+    @open="modalBeforeOpen"
+  >
+    <el-form
+      ref="formRef"
+      :model="formModel"
+      :rules="rules"
+      label-width="220px"
+    >
+      <el-form-item label="评卷时强制试卷拉到底部">
+        <el-checkbox v-model="formModel.forceScrollToBottom"> </el-checkbox>
+      </el-form-item>
+
+      <el-form-item label="最小阅卷时长(秒)" prop="minMarkingDuration">
+        <el-input-number
+          v-model="formModel.minMarkingDuration"
+          :min="0"
+          :max="999999"
+          :step="1"
+          :precision="0"
+          :controls="false"
+          step-strictly
+          placeholder="请输入最小阅卷时长"
+          style="width: 200px"
+        />
+      </el-form-item>
+
+      <el-form-item label="单卷回评次数" prop="singlePaperRecheckCount">
+        <el-input-number
+          v-model="formModel.singlePaperRecheckCount"
+          :min="0"
+          :max="100"
+          :step="1"
+          :precision="0"
+          :controls="false"
+          step-strictly
+          placeholder="请输入单卷回评次数"
+          style="width: 200px"
+        />
+      </el-form-item>
+
+      <el-form-item label="给分次数限制" prop="scoringLimitType">
+        <el-select
+          v-model="formModel.scoringLimitType"
+          placeholder="请选择给分次数限制类型"
+          style="width: 200px"
+          clearable
+        >
+          <el-option label="小于等于" value="le" />
+          <el-option label="等于" value="eq" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="启用条码粘贴AI检测">
+        <el-checkbox v-model="formModel.enableBarcodeAIDetection">
+        </el-checkbox>
+        <span v-show="formModel.enableBarcodeAIDetection" class="tip-text"
+          >预计耗时: 15小时12分</span
+        >
+        <div class="ai-detection-tip">
+          <span class="tip-warning">
+            *AI检测均耗时较久,如需使用此功能,建议在开始扫描时就配置启用此功能
+          </span>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="启用题卡作答AI检测">
+        <el-checkbox v-model="formModel.enableAnswerSheetAIDetection">
+        </el-checkbox>
+        <div class="ai-detection-tip">
+          <span class="tip-warning">
+            *AI检测均耗时较久,如需使用此功能,建议在开始扫描时就配置启用此功能
+          </span>
+        </div>
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="close">返回</el-button>
+        <el-button type="primary" :loading="loading" @click="confirm">
+          保存
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive } from 'vue';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import type { ExamAdvancedConfig } from '@/api/types/exam';
+  import useModal from '@/hooks/modal';
+  import useLoading from '@/hooks/loading';
+  import { objAssign, objModifyAssign } from '@/utils/utils';
+
+  defineOptions({
+    name: 'ExamAdvancedDialog',
+  });
+
+  interface Props {
+    advancedConfig?: ExamAdvancedConfig;
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    advancedConfig: () => ({
+      forceScrollToBottom: false,
+      minMarkingDuration: undefined,
+      singlePaperRecheckCount: undefined,
+      scoringLimitType: null,
+      enableBarcodeAIDetection: false,
+      enableAnswerSheetAIDetection: false,
+    }),
+  });
+
+  const emit = defineEmits<{
+    confirm: [config: ExamAdvancedConfig];
+  }>();
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  const formRef = ref<FormInstance>();
+  const { loading, setLoading } = useLoading();
+
+  const initialFormState: ExamAdvancedConfig = {
+    forceScrollToBottom: false,
+    minMarkingDuration: undefined,
+    singlePaperRecheckCount: undefined,
+    scoringLimitType: null,
+    enableBarcodeAIDetection: false,
+    enableAnswerSheetAIDetection: false,
+  };
+
+  const formModel = reactive({ ...initialFormState });
+
+  const rules: FormRules<keyof ExamAdvancedConfig> = {
+    minMarkingDuration: [
+      {
+        validator: (rule, value, callback) => {
+          if (value !== undefined && value < 0) {
+            callback(new Error('最小阅卷时长不能小于0'));
+          } else {
+            callback();
+          }
+        },
+        trigger: 'blur',
+      },
+    ],
+    singlePaperRecheckCount: [
+      {
+        validator: (rule, value, callback) => {
+          if (value !== undefined && value < 0) {
+            callback(new Error('单卷回评次数不能小于0'));
+          } else {
+            callback();
+          }
+        },
+        trigger: 'blur',
+      },
+    ],
+  };
+
+  const handleClose = () => {
+    formRef.value?.resetFields();
+  };
+
+  /* confirm */
+  async function confirm() {
+    const valid = await formRef.value?.validate().catch(() => false);
+    if (!valid) return;
+
+    setLoading(true);
+    try {
+      const config = objAssign(formModel, {}) as ExamAdvancedConfig;
+      emit('confirm', config);
+      close();
+    } catch (error) {
+      console.error('保存高级设置失败:', error);
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  /* init modal */
+  function modalBeforeOpen() {
+    if (props.advancedConfig) {
+      objModifyAssign(formModel, props.advancedConfig);
+    } else {
+      objModifyAssign(formModel, initialFormState);
+    }
+  }
+</script>
+
+<style scoped>
+  .ai-detection-tip {
+    width: 100%;
+  }
+
+  .tip-text {
+    color: #666;
+    margin-right: 10px;
+  }
+
+  .tip-warning {
+    color: #e6a23c;
+  }
+
+  .dialog-footer .el-button {
+    margin: 0 5px;
+  }
+</style>

+ 511 - 0
src/views/exam/ExamEdit.vue

@@ -0,0 +1,511 @@
+<template>
+  <div class="exam-edit-page">
+    <div class="page-header">
+      <h2>{{ isEdit ? '编辑考试' : '新增考试' }}</h2>
+    </div>
+
+    <div class="page-content">
+      <el-form
+        ref="formRef"
+        :model="formModel"
+        :rules="rules"
+        label-width="280px"
+        class="exam-form"
+      >
+        <!-- 考试信息 -->
+        <div class="form-section">
+          <h3>考试信息</h3>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="考试名称" prop="name">
+                <el-input
+                  v-model="formModel.name"
+                  placeholder="请输入考试名称"
+                  style="width: 200px"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="类型" prop="type" label-width="200px">
+                <el-select
+                  v-model="formModel.type"
+                  placeholder="请选择类型"
+                  style="width: 200px"
+                >
+                  <el-option label="扫描图片" value="扫描图片" />
+                  <el-option label="其他类型" value="其他类型" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="评卷开始日期">
+                <el-date-picker
+                  v-model="formModel.markingStartDate"
+                  type="date"
+                  placeholder="请选择评卷开始日期"
+                  style="width: 200px"
+                  format="YYYY-MM-DD"
+                  value-format="YYYY-MM-DD"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="评卷结束日期" label-width="200px">
+                <el-date-picker
+                  v-model="formModel.markingEndDate"
+                  type="date"
+                  placeholder="请选择评卷结束日期"
+                  style="width: 200px"
+                  format="YYYY-MM-DD"
+                  value-format="YYYY-MM-DD"
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <el-row v-if="isEdit" :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="状态">
+                <el-select
+                  v-model="formModel.status"
+                  placeholder="请选择"
+                  style="width: 200px"
+                >
+                  <el-option label="A4" value="1" />
+                  <el-option label="A3" value="2" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+
+        <!-- 阅卷设置 -->
+        <div class="form-section">
+          <div class="section-header">
+            <h3>阅卷设置</h3>
+            <el-button type="primary" link @click="openAdvancedSettings">
+              高级设置
+            </el-button>
+          </div>
+
+          <h4 class="section-sub-title">评卷给分方式</h4>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="评卷模式" prop="markingMode">
+                <el-select
+                  v-model="formModel.markingMode"
+                  placeholder="请选择"
+                  style="width: 200px"
+                >
+                  <el-option label="不限" value="NONE" />
+                  <el-option label="轨迹" value="TRACK" />
+                  <el-option label="普通" value="COMMON" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="强制标记" label-width="200px">
+                <el-checkbox v-model="formModel.forceMark"></el-checkbox>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <h4 class="section-sub-title">数据安全</h4>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="禁止他人查看考生信息">
+                <el-checkbox
+                  v-model="formModel.forbidViewStudentInfo"
+                ></el-checkbox>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="禁止科组长成绩查询" label-width="200px">
+                <el-checkbox
+                  v-model="formModel.forbidLeaderScoreQuery"
+                ></el-checkbox>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <h4 class="section-sub-title">全卷复核设置</h4>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="全卷多次复核不能同一账号">
+                <el-checkbox
+                  v-model="formModel.preventSameAccountRecheck"
+                ></el-checkbox>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item
+                label="全卷复核强制试卷拉到底部"
+                label-width="200px"
+              >
+                <el-checkbox
+                  v-model="formModel.forceScrollToBottom"
+                ></el-checkbox>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="全卷复核进度100%时才能再次复核">
+                <el-checkbox
+                  v-model="formModel.requireFullProgressForRecheck"
+                ></el-checkbox>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <h4 class="section-sub-title">其他</h4>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="回评卷数" prop="recheckCount">
+                <el-input-number
+                  v-model="formModel.recheckCount"
+                  :min="0"
+                  :max="100"
+                  :step="1"
+                  :precision="0"
+                  :controls="false"
+                  step-strictly
+                  style="width: 100px"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item
+                label="给分次数限制"
+                prop="scoringLimitType"
+                label-width="200px"
+              >
+                <el-select
+                  v-model="formModel.scoringLimitType"
+                  placeholder="请选择"
+                  style="width: 200px"
+                >
+                  <el-option label="小于等于" value="le" />
+                  <el-option label="等于" value="eq" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="评卷端显示客观分">
+                <el-checkbox
+                  v-model="formModel.showObjectiveScore"
+                ></el-checkbox>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="原图遮盖" label-width="200px">
+                <el-button type="primary" link>设置</el-button>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="打回后显示原分值及轨迹">
+                <el-checkbox
+                  v-model="formModel.showOriginalScoreAfterReturn"
+                ></el-checkbox>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="原卷显示" label-width="200px">
+                <el-checkbox
+                  v-model="formModel.showOriginalPaper"
+                ></el-checkbox>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="评卷提交自动定位">
+                <el-checkbox
+                  v-model="formModel.autoPositionAfterSubmit"
+                ></el-checkbox>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="自动对切题卡" label-width="200px">
+                <el-checkbox
+                  v-model="formModel.autoAlignAnswerSheet"
+                ></el-checkbox>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <h4 class="section-sub-title">数据分析设置</h4>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="及格分" prop="passingScore">
+                <el-input-number
+                  v-model="formModel.passingScore"
+                  :min="0"
+                  :max="100"
+                  :step="1"
+                  :precision="0"
+                  :controls="false"
+                  step-strictly
+                  style="width: 100px"
+                />
+                <span class="input-suffix">%</span>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item
+                label="优秀分"
+                prop="excellentScore"
+                label-width="200px"
+              >
+                <el-input-number
+                  v-model="formModel.excellentScore"
+                  :min="0"
+                  :max="100"
+                  :step="1"
+                  :precision="0"
+                  :controls="false"
+                  step-strictly
+                  style="width: 100px"
+                />
+                <span class="input-suffix">%</span>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="20">
+              <el-form-item label="备注描述">
+                <el-input
+                  v-model="formModel.description"
+                  type="textarea"
+                  :rows="4"
+                  maxlength="999"
+                  placeholder="请输入备注描述"
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+        <div class="form-actions">
+          <el-button @click="goBack">返回</el-button>
+          <el-button type="primary" :loading="loading" @click="handleSave">
+            保存
+          </el-button>
+        </div>
+      </el-form>
+    </div>
+
+    <!-- 高级设置弹窗 -->
+    <ExamAdvancedDialog
+      ref="advancedDialogRef"
+      :advanced-config="advancedConfig"
+      @confirm="handleAdvancedConfirm"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, computed, onMounted } from 'vue';
+  import { useRoute, useRouter } from 'vue-router';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import { ElMessage } from 'element-plus';
+  import { getExamDetail, updateExam } from '@/api/exam';
+  import type { ExamUpdateParam, ExamAdvancedConfig } from '@/api/types/exam';
+  import useLoading from '@/hooks/loading';
+  import ExamAdvancedDialog from './ExamAdvancedDialog.vue';
+
+  defineOptions({
+    name: 'ExamEdit',
+  });
+
+  const route = useRoute();
+  const router = useRouter();
+  const { loading, setLoading } = useLoading();
+
+  const examId = computed(() => Number(route.query.id) || 0);
+  const isEdit = computed(() => !!examId.value);
+
+  const formRef = ref<FormInstance>();
+  const advancedDialogRef = ref();
+
+  // 表单数据
+  const initialFormState: Partial<ExamUpdateParam> = {
+    name: '',
+    type: '',
+    scanImageType: 1,
+    examDate: '',
+    markingStartDate: '',
+    markingEndDate: '',
+    status: '',
+    markingMode: 'TRACK',
+    forceMark: false,
+    forbidViewStudentInfo: false,
+    forbidLeaderScoreQuery: false,
+    preventSameAccountRecheck: false,
+    forceScrollToBottom: false,
+    requireFullProgressForRecheck: false,
+    showObjectiveScore: false,
+    showOriginalScoreAfterReturn: false,
+    recheckCount: undefined,
+    passingScore: 60,
+    excellentScore: 90,
+    showOriginalPaper: false,
+    autoPositionAfterSubmit: false,
+    autoAlignAnswerSheet: false,
+    description: '',
+    // 高级配置默认值
+    minMarkingDuration: undefined,
+    singlePaperRecheckCount: undefined,
+    scoringLimitType: null,
+    scoringLimitCount: undefined,
+    enableBarcodeAIDetection: false,
+    enableAnswerSheetAIDetection: false,
+  };
+
+  const formModel = reactive({ ...initialFormState });
+
+  // 高级配置数据
+  const advancedConfig = computed<ExamAdvancedConfig>(() => ({
+    forceScrollToBottom: formModel.forceScrollToBottom || false,
+    minMarkingDuration: formModel.minMarkingDuration,
+    singlePaperRecheckCount: formModel.singlePaperRecheckCount,
+    scoringLimitType: formModel.scoringLimitType,
+    scoringLimitCount: formModel.scoringLimitCount,
+    enableBarcodeAIDetection: formModel.enableBarcodeAIDetection || false,
+    enableAnswerSheetAIDetection:
+      formModel.enableAnswerSheetAIDetection || false,
+  }));
+
+  // 表单验证规则
+  const rules: FormRules<keyof ExamUpdateParam> = {
+    name: [{ required: true, message: '请输入考试名称', trigger: 'change' }],
+    type: [{ required: true, message: '请选择类型', trigger: 'change' }],
+  };
+
+  // 加载考试详情
+  async function loadExamDetail() {
+    if (!examId.value) return;
+
+    setLoading(true);
+    try {
+      const result = await getExamDetail(examId.value);
+      Object.assign(formModel, result);
+    } catch (error) {
+      console.error('获取考试详情失败:', error);
+      ElMessage.error('获取考试详情失败');
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  // 打开高级设置弹窗
+  function openAdvancedSettings() {
+    advancedDialogRef.value?.open();
+  }
+
+  // 高级设置确认回调
+  function handleAdvancedConfirm(config: ExamAdvancedConfig) {
+    Object.assign(formModel, config);
+  }
+
+  // 保存
+  async function handleSave() {
+    const valid = await formRef.value?.validate().catch(() => false);
+    if (!valid) return;
+
+    setLoading(true);
+    try {
+      const data = { ...formModel } as ExamUpdateParam;
+
+      await updateExam(data);
+      ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功!`);
+      goBack();
+    } catch (error) {
+      console.error('保存失败:', error);
+      ElMessage.error('保存失败');
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  // 返回
+  function goBack() {
+    router.back();
+  }
+
+  onMounted(() => {
+    loadExamDetail();
+  });
+</script>
+
+<style scoped>
+  .page-header {
+    margin-bottom: 20px;
+  }
+
+  .page-header h2 {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 500;
+  }
+
+  .exam-form {
+    max-width: 1200px;
+  }
+
+  .form-section {
+    margin-bottom: 30px;
+    padding: 20px;
+    background: #fff;
+    border-radius: 6px;
+  }
+
+  .form-section h3 {
+    margin: 0 0 20px 0;
+    font-size: 16px;
+    font-weight: 500;
+    color: #333;
+  }
+
+  .section-sub-title {
+    margin: 10px 0;
+    font-weight: 500;
+    color: #999;
+  }
+
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+  }
+
+  .section-header h3 {
+    margin: 0;
+  }
+
+  .input-suffix {
+    margin-left: 8px;
+    color: #666;
+  }
+
+  .form-actions {
+    margin-top: 30px;
+    text-align: center;
+  }
+
+  .form-actions .el-button {
+    margin: 0 10px;
+    min-width: 100px;
+  }
+</style>

+ 166 - 0
src/views/exam/ExamManage.vue

@@ -0,0 +1,166 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="考试名称">
+        <el-input
+          v-model.trim="searchModel.name"
+          placeholder="请输入"
+          clearable
+          style="width: 200px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="类型">
+        <el-select
+          v-model="searchModel.type"
+          placeholder="不限"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="类型1" value="1" />
+          <el-option label="类型2" value="2" />
+          <el-option label="类型3" value="3" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-select
+          v-model="searchModel.status"
+          placeholder="不限"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="已启用" value="1" />
+          <el-option label="已禁用" value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="toPage(1)">查询</el-button>
+        <el-button type="primary" @click="onAdd">新建考试</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column property="id" label="ID" width="80" />
+      <el-table-column property="name" label="考试名称" min-width="200">
+        <template #default="scope">
+          <el-button type="pirmary" link @click="onViewDetail(scope.row)">{{
+            scope.row.name
+          }}</el-button>
+        </template>
+      </el-table-column>
+      <el-table-column property="type" label="类型" width="100">
+        <template #default="scope">
+          {{ getTypeLabel(scope.row.type) }}
+        </template>
+      </el-table-column>
+      <el-table-column property="examDate" label="考试日期" width="120" />
+      <el-table-column property="forceMark" label="强制标记" width="100">
+        <template #default="scope">
+          <el-tag :type="scope.row.forceMark ? 'warning' : 'info'">
+            {{ scope.row.forceMark ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column property="status" label="状态" width="100">
+        <template #default="scope">
+          <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
+            {{ scope.row.status === 1 ? '已启用' : '已禁用' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="操作" width="200" fixed="right">
+        <template #default="scope">
+          <el-button size="small" link @click="onViewDetail(scope.row)">
+            详情
+          </el-button>
+          <el-button size="small" link @click="onEdit(scope.row)">
+            编辑
+          </el-button>
+          <el-button size="small" link @click="onCopy(scope.row)">
+            复制
+          </el-button>
+          <el-button size="small" link @click="onAnalysis(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>
+
+  <!-- 统计信息弹窗 -->
+  <ExamStatDialog ref="statDialogRef" :exam-id="currentExamId" />
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref } from 'vue';
+  import { useRouter } from 'vue-router';
+  import { getExamList } from '@/api/exam';
+  import { ExamQueryItem, ExamListFilter } from '@/api/types/exam';
+  import useTable from '@/hooks/table';
+  import ExamStatDialog from './ExamStatDialog.vue';
+
+  defineOptions({
+    name: 'ExamManage',
+  });
+
+  const router = useRouter();
+
+  const searchModel = reactive<ExamListFilter>({
+    name: '',
+    type: '',
+    status: '',
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<ExamQueryItem>(getExamList, searchModel, false);
+
+  function getTypeLabel(type: number): string {
+    const typeMap: Record<number, string> = {
+      1: '类型1',
+      2: '类型2',
+      3: '类型3',
+    };
+    return typeMap[type] || '未知';
+  }
+
+  // action相关
+  const currentExamId = ref(0);
+  // 详情统计弹窗
+  const statDialogRef = ref();
+  function onViewDetail(row: ExamQueryItem) {
+    currentExamId.value = row.id;
+    statDialogRef.value?.open();
+  }
+
+  function onAdd() {
+    router.push({ name: 'ExamEdit' });
+  }
+
+  function onEdit(row: ExamQueryItem) {
+    router.push({
+      name: 'ExamEdit',
+      query: { id: row.id },
+    });
+  }
+
+  function onCopy(row: ExamQueryItem) {
+    // TODO: 实现复制功能
+    console.log('复制考试:', row);
+  }
+
+  function onAnalysis(row: ExamQueryItem) {
+    // TODO: 实现分析功能
+    console.log('分析考试:', row);
+  }
+</script>

+ 143 - 0
src/views/exam/ExamStatDialog.vue

@@ -0,0 +1,143 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="考试统计信息"
+    width="600px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    top="10vh"
+    append-to-body
+    @open="modalBeforeOpen"
+  >
+    <div v-loading="loading">
+      <el-table :data="tableData" style="width: 100%">
+        <el-table-column prop="name" label="名称" width="100" />
+        <el-table-column prop="attribute" label="属性" min-width="200" />
+        <el-table-column label="操作" width="80">
+          <template #default="scope">
+            <el-button type="primary" link @click="handleAction(scope.row)">
+              进入
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-button @click="onViewReport">验收报告</el-button>
+    </div>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="close">关闭</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed } from 'vue';
+  import { useRouter } from 'vue-router';
+  import { getExamStatisticInfo } from '@/api/exam';
+  import { ExamStatDetailInfo } from '@/api/types/exam';
+  import useModal from '@/hooks/modal';
+  import useLoading from '@/hooks/loading';
+
+  defineOptions({
+    name: 'ExamStatDialog',
+  });
+
+  interface Props {
+    examId?: number;
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    examId: 0,
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  const { loading, setLoading } = useLoading();
+
+  const router = useRouter;
+
+  const statInfo = ref<ExamStatDetailInfo>({
+    studentCount: 0,
+    subjectCount: 0,
+    scanProgress: 0,
+    scanImageCount: 0,
+    markingProgress: 0,
+  });
+
+  interface TableDataItem {
+    name: string;
+    attribute: string;
+    type: string;
+    router: string;
+  }
+
+  const tableData = computed<TableDataItem[]>(() => [
+    {
+      name: '考生',
+      attribute: `共录入 ${statInfo.value.studentCount} 个考生`,
+      type: 'student',
+      router: 'StudentManage',
+    },
+    {
+      name: '科目',
+      attribute: `共录入 ${statInfo.value.subjectCount} 个科目`,
+      type: 'subject',
+      router: 'SubjectManage',
+    },
+    {
+      name: '扫描进度',
+      attribute: `已扫描 ${statInfo.value.scanProgress}%`,
+      type: 'scanProgress',
+      router: 'ScanManage',
+    },
+    {
+      name: '扫描张数',
+      attribute: `已扫描 ${statInfo.value.scanImageCount} 张`,
+      type: 'scanImage',
+      router: 'ScanManage',
+    },
+    {
+      name: '评卷进度',
+      attribute: `已评卷 ${statInfo.value.markingProgress}%`,
+      type: 'markingProgress',
+      router: 'MarkProgress',
+    },
+  ]);
+
+  async function loadStatInfo() {
+    if (!props.examId) return;
+
+    setLoading(true);
+    try {
+      const result = await getExamStatisticInfo(props.examId);
+      statInfo.value = result;
+    } catch (error) {
+      console.error('获取统计信息失败:', error);
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  function handleAction(row: TableDataItem) {
+    router.push({
+      name: row.router,
+      // TODO: 这里是否直接切换全局的科目
+      query: {
+        examId: props.examId,
+      },
+    });
+  }
+
+  function onViewReport() {
+    // TODO: 查看报告
+    console.log('查看报告');
+  }
+
+  /* init modal */
+  function modalBeforeOpen() {
+    loadStatInfo();
+  }
+</script>