瀏覽代碼

feat: 科目管理

zhangjie 3 天之前
父節點
當前提交
d7b15f3f90

+ 1 - 0
components.d.ts

@@ -53,6 +53,7 @@ declare module '@vue/runtime-core' {
     ImportDialog: typeof import('./src/components/import-dialog/index.vue')['default'];
     RouterLink: typeof import('vue-router')['RouterLink'];
     RouterView: typeof import('vue-router')['RouterView'];
+    SelectCourse: typeof import('./src/components/select-course/index.vue')['default'];
     SelectExam: typeof import('./src/components/select-exam/index.vue')['default'];
     SelectRangeDatetime: typeof import('./src/components/select-range-datetime/index.vue')['default'];
     SelectRangeTime: typeof import('./src/components/select-range-time/index.vue')['default'];

+ 64 - 0
src/api/subject.ts

@@ -0,0 +1,64 @@
+import axios from 'axios';
+import {
+  SubjectTotalScoreStatItem,
+  SubjectListPageParam,
+  SubjectListPageRes,
+  SubjectSettingInfo,
+  PaperStructureListPageRes,
+  PaperStructureItem,
+  PaperStructureUpdateParams,
+  SubjectItem,
+} from './types/subject';
+
+// 科目管理
+// 获取科目总分统计列表
+export function getSubjectTotalScoreStatList(
+  examId: number
+): Promise<SubjectTotalScoreStatItem[]> {
+  return axios.get(`/api/subject/total-score-stat/${examId}`);
+}
+
+// 获取科目列表
+export function getSubjectList(
+  params: SubjectListPageParam
+): Promise<SubjectListPageRes> {
+  return axios.post('/api/subject/list', { params });
+}
+// 获取科目详情
+export function getSubjectDetail(subjectId: number): Promise<SubjectItem> {
+  return axios.post('/api/subject/list', { params: { subjectId } });
+}
+
+// 获取科目试卷结构列表
+export function getPaperStructureList(
+  subjectId: number
+): Promise<PaperStructureListPageRes> {
+  return axios.post(`/api/subject/paper-structure/${subjectId}`);
+}
+
+// 获取科目设置信息
+export function getSubjectSetting(
+  subjectId: number
+): Promise<SubjectSettingInfo> {
+  return axios.post('/api/subject/setting', { params: { subjectId } });
+}
+// 保存科目设置信息
+export function saveSubjectSetting(
+  data: SubjectSettingInfo
+): Promise<SubjectSettingInfo> {
+  return axios.post('/api/subject/setting', data);
+}
+
+// 新增/编辑试卷结构
+export function savePaperStructure(
+  data: PaperStructureUpdateParams
+): Promise<PaperStructureItem> {
+  return axios.post('/api/subject/paper-structure', data);
+}
+
+// 批量删除试卷结构
+export function deletePaperStructures(ids: number[]): Promise<boolean> {
+  return axios.post('/api/subject/paper-structure/batch', {
+    data: { ids },
+  });
+}

+ 9 - 0
src/api/types/common.ts

@@ -15,3 +15,12 @@ export interface AbleParams {
   id: number;
   enable: boolean;
 }
+
+// 遮盖区域
+export interface CoverArea {
+  i: number;
+  x: number;
+  y: number;
+  w: number;
+  h: number;
+}

+ 111 - 0
src/api/types/subject.ts

@@ -0,0 +1,111 @@
+import { PageResult, PageParams, CoverArea } from './common';
+
+export interface SubjectTotalScoreStatItem {
+  // 总分
+  totalScore: number;
+  // 数量
+  count: number;
+}
+
+// 科目列表: 科目名称	层次	专业类型	选做科目	试卷	答案	试卷类型	卡格式	客观总分	主观总分	试卷总分	状态
+export interface SubjectItem {
+  id: number;
+  // 科目名称
+  name: string;
+  // 科目代码
+  code: string;
+  // 层次
+  level: string;
+  // 专业类型
+  majorType: string;
+  // 选做科目
+  optional: boolean;
+  // 试卷
+  paperUrl: string;
+  // 答案
+  answerUrl: string;
+  // 试卷类型
+  paperType: string;
+  // 卡格式
+  cardFormatType: string;
+  // 客观总分
+  objectiveTotalScore: number;
+  // 主观总分
+  subjectiveTotalScore: number;
+  // 试卷总分
+  paperTotalScore: number;
+  // 状态
+  status: string;
+}
+export type SubjectListPageRes = PageResult<SubjectItem>;
+
+export interface SubjectListFilter {
+  // 科目名称
+  subject?: number | null;
+  // 层次
+  level?: string;
+  // 专业类型
+  majorType?: string;
+  // 选做科目
+  optional?: boolean;
+  // 状态
+  status: string;
+  // 总分
+  totalScore?: number;
+}
+export type SubjectListPageParam = PageParams<SubjectListFilter>;
+
+// 科目设置信息
+export interface SubjectSettingInfo {
+  id: number;
+  // 科目名称
+  name: string;
+  // 科目代码
+  code: string;
+  // 评卷显示题目昵称
+  showQuestionNickname: boolean;
+  // 及格分
+  passScore: number;
+  // 优秀分
+  excellentScore: number;
+  // 原图遮盖
+  sheetConfig: CoverArea[] | null;
+  // 评卷提交自动定位
+  autoLocate: boolean;
+  // 自动对切题卡
+  autoCut: boolean;
+}
+
+// 试卷结构列表:试卷类型	大题名称	大题昵称	大题号	小题号	满分	给分次数 是否客观题	间隔分 题型	答案	判分策略	分组
+export interface PaperStructureItem {
+  id: number;
+  // 试卷类型
+  paperType: string;
+  // 大题名称
+  bigQuestionName: string;
+  // 大题昵称
+  bigQuestionNickname: string;
+  // 大题号
+  bigQuestionNo: number;
+  // 小题号
+  smallQuestionNo: number;
+  // 满分
+  fullScore: number;
+  // 给分次数
+  giveScoreCount: number;
+  // 是否客观题
+  isObjective: boolean;
+  // 间隔分
+  intervalScore: number;
+  // 题型
+  questionType: string;
+  // 答案
+  answer: string;
+  // 判分策略
+  scoringStrategy: string;
+  // 分组
+  grouping: string;
+}
+export type PaperStructureListPageRes = PaperStructureItem[];
+
+export type PaperStructureUpdateParams = Partial<PaperStructureItem>;

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

@@ -36,6 +36,24 @@ const BASE: AppRouteRecordRaw = {
         requiresAuth: true,
       },
     },
+    {
+      path: '/subject-manage',
+      name: 'SubjectManage',
+      component: () => import('@/views/subject/SubjectManage.vue'),
+      meta: {
+        title: '科目管理',
+        requiresAuth: true,
+      },
+    },
+    {
+      path: '/subject/paper-struct-edit/:subjectId',
+      name: 'PaperStructEdit',
+      component: () => import('@/views/subject/PaperStructEdit.vue'),
+      meta: {
+        title: '试卷结构编辑',
+        requiresAuth: true,
+      },
+    },
     {
       path: '/exam-manage',
       name: 'ExamManage',

+ 287 - 0
src/views/subject/PaperStructEdit.vue

@@ -0,0 +1,287 @@
+<template>
+  <div class="part-box is-filter">
+    <div class="subject-info">
+      <span>科目:{{ subjectInfo.code }}-{{ subjectInfo.name }}</span>
+      <span style="margin-left: 20px"
+        >客观总分:{{ subjectInfo.objectiveTotalScore }}</span
+      >
+      <span style="margin-left: 20px"
+        >主观总分:{{ subjectInfo.subjectiveTotalScore }}</span
+      >
+      <span style="margin-left: 20px"
+        >试卷总分:{{ subjectInfo.paperTotalScore }}</span
+      >
+    </div>
+
+    <el-form inline>
+      <el-form-item label="试卷类型">
+        <el-select
+          v-model="searchModel.paperType"
+          placeholder="不限"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="不限" value="" />
+          <el-option label="A" value="A" />
+          <el-option label="B" value="B" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="大题">
+        <el-select
+          v-model="searchModel.bigQuestionNo"
+          placeholder="不限"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="不限" value="" />
+          <el-option label="1" :value="1" />
+          <el-option label="2" :value="2" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="onAdd">新增</el-button>
+          <el-button
+            type="danger"
+            :disabled="!hasSelection"
+            @click="onBatchDelete"
+          >
+            删除
+          </el-button>
+          <el-button @click="onReturn">返回</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+
+  <div class="part-box">
+    <el-table
+      ref="tableRef"
+      v-loading="loading"
+      :data="dataList"
+      class="page-table"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column
+        type="selection"
+        width="55"
+        :selectable="(row) => !row.grouping"
+      />
+      <el-table-column prop="paperType" label="试卷类型" width="80" />
+      <el-table-column prop="bigQuestionName" label="大题名称" width="120" />
+      <el-table-column
+        prop="bigQuestionNickname"
+        label="大题昵称"
+        width="120"
+      />
+      <el-table-column prop="bigQuestionNo" label="大题号" width="80" />
+      <el-table-column prop="smallQuestionNo" label="小题号" width="80" />
+      <el-table-column prop="fullScore" label="满分" width="80" />
+      <el-table-column prop="giveScoreCount" label="给分次数" width="100" />
+      <el-table-column label="是否客观题" width="100">
+        <template #default="{ row }">
+          <el-tag :type="row.isObjective ? 'success' : 'info'">
+            {{ row.isObjective ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="intervalScore" label="间隔分" width="80" />
+      <el-table-column prop="questionType" label="题型" width="100" />
+      <el-table-column prop="answer" label="答案" width="100" />
+      <el-table-column prop="scoringStrategy" label="判分策略" width="100" />
+      <el-table-column prop="grouping" label="分组" width="80">
+        <template #default="{ row }">
+          <span v-if="row.grouping">{{ row.grouping }}</span>
+          <span v-else style="color: #999">无</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="100" fixed="right">
+        <template #default="{ row }">
+          <el-button size="small" type="primary" link @click="onEdit(row)">
+            编辑
+          </el-button>
+          <el-button
+            v-if="!row.grouping"
+            size="small"
+            type="danger"
+            link
+            @click="onDelete(row)"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+
+  <!-- 新增/编辑试卷结构弹窗 -->
+  <ModifyPaperStructure
+    ref="modifyPaperStructureRef"
+    :row-data="curRow"
+    :subject-id="subjectId"
+    @modified="getList"
+  />
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref, computed, onMounted, watch } from 'vue';
+  import { useRoute, useRouter } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+  import {
+    getSubjectDetail,
+    getPaperStructureList,
+    deletePaperStructures,
+  } from '@/api/subject';
+  import type { PaperStructureItem } from '@/api/types/subject';
+  import useLoading from '@/hooks/loading';
+  import { modalConfirm } from '@/utils/ui';
+
+  import ModifyPaperStructure from './components/ModifyPaperStructure.vue';
+
+  defineOptions({
+    name: 'PaperStructEdit',
+  });
+
+  const route = useRoute();
+  const router = useRouter();
+
+  // 从路由参数获取科目ID
+  const subjectId = computed(() => Number(route.params.subjectId) || 0);
+
+  const subjectInfo = reactive({
+    code: '',
+    name: '',
+    objectiveTotalScore: 0,
+    subjectiveTotalScore: 0,
+    paperTotalScore: 0,
+  });
+  // 获取科目详情信息
+  const getSubjectDetailInfo = async () => {
+    const res = await getSubjectDetail(subjectId.value);
+    subjectInfo.code = res.code;
+    subjectInfo.name = res.name;
+    subjectInfo.objectiveTotalScore = res.objectiveTotalScore;
+    subjectInfo.subjectiveTotalScore = res.subjectiveTotalScore;
+    subjectInfo.paperTotalScore = res.paperTotalScore;
+  };
+
+  interface SearchModel {
+    paperType: string;
+    bigQuestionNo: number | string;
+  }
+
+  const searchModel = reactive<SearchModel>({
+    paperType: '',
+    bigQuestionNo: '',
+  });
+
+  // 获取试卷结构列表
+  const { loading, setLoading } = useLoading();
+  const structList = ref<PaperStructureItem[]>([]);
+  const dataList = ref<PaperStructureItem[]>([]);
+  // 更新dataList
+  const updateDataList = () => {
+    const { paperType, bigQuestionNo } = searchModel;
+    dataList.value = structList.value.filter((item) => {
+      return (
+        (!paperType || item.paperType === paperType) &&
+        (!bigQuestionNo || item.bigQuestionNo === bigQuestionNo)
+      );
+    });
+  };
+  // 获取试卷结构列表
+  const getList = async () => {
+    setLoading(true);
+    const res = await getPaperStructureList(subjectId.value).catch(() => []);
+    setLoading(false);
+    structList.value = res || [];
+    updateDataList();
+  };
+
+  watch(
+    () => searchModel,
+    () => {
+      updateDataList();
+    }
+  );
+
+  // 表格选择相关
+  const selectedRows = ref<PaperStructureItem[]>([]);
+  const hasSelection = computed(() => selectedRows.value.length > 0);
+
+  const handleSelectionChange = (selection: PaperStructureItem[]) => {
+    selectedRows.value = selection;
+  };
+
+  // 新增/编辑相关
+  const curRow = ref({} as PaperStructureItem);
+  const modifyPaperStructureRef = ref();
+
+  const onAdd = () => {
+    curRow.value = {} as PaperStructureItem;
+    modifyPaperStructureRef.value?.open();
+  };
+
+  const onEdit = (row: PaperStructureItem) => {
+    curRow.value = row;
+    modifyPaperStructureRef.value?.open();
+  };
+
+  // 删除相关
+  const onDelete = async (row: PaperStructureItem) => {
+    const confirm = await modalConfirm(
+      '确定要删除这条试卷结构记录吗?',
+      '提示'
+    ).catch(() => false);
+    if (!confirm) return;
+
+    try {
+      await deletePaperStructures([row.id]);
+      ElMessage.success('删除成功');
+      getList();
+    } catch (error) {
+      console.error('删除失败:', error);
+    }
+  };
+
+  const onBatchDelete = async () => {
+    const confirm = await modalConfirm(
+      `确定要删除选中的 ${selectedRows.value.length} 条记录吗?`,
+      '提示'
+    ).catch(() => false);
+    if (!confirm) return;
+
+    try {
+      const ids = selectedRows.value.map((row) => row.id);
+      await deletePaperStructures(ids);
+      ElMessage.success('批量删除成功');
+      selectedRows.value = [];
+      getList();
+    } catch (error) {
+      console.error('批量删除失败:', error);
+    }
+  };
+
+  const onReturn = () => {
+    router.back();
+  };
+
+  onMounted(() => {
+    if (subjectId.value) {
+      getList();
+      getSubjectDetailInfo();
+    }
+  });
+</script>
+
+<style scoped>
+  .subject-info {
+    padding: 16px;
+    background-color: #f5f7fa;
+    border-radius: 4px;
+    margin-bottom: 16px;
+    font-size: 14px;
+    color: #606266;
+  }
+</style>

+ 404 - 0
src/views/subject/SubjectManage.vue

@@ -0,0 +1,404 @@
+<template>
+  <div class="part-box">
+    <!-- 科目总分统计图表 -->
+    <div class="chart-container">
+      <h3>科目总分统计</h3>
+      <Chart ref="chartRef" :option="chartOption" style="height: 300px" />
+    </div>
+  </div>
+
+  <div class="part-box is-filter">
+    <el-form :model="searchModel" inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item label="层次">
+        <el-select
+          v-model="searchModel.level"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="高中" value="高中" />
+          <el-option label="初中" value="初中" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="选做科目">
+        <el-select
+          v-model="searchModel.optional"
+          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.majorType"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="理科" value="理科" />
+          <el-option label="文科" value="文科" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-select
+          v-model="searchModel.status"
+          placeholder="不限"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="正常" value="正常" />
+          <el-option label="异常" value="异常" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="总分">
+        <el-input-number
+          v-model.number="searchModel.totalScore"
+          :min="0"
+          :max="9999"
+          :step="0.1"
+          :precision="1"
+          :controls="false"
+          step-strictly
+          placeholder="请输入"
+          style="width: 120px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-dropdown @command="onImportCommand">
+            <el-button type="primary">
+              导入
+              <el-icon class="el-icon--right"><ArrowDown /> </el-icon>
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="objective">客观题</el-dropdown-item>
+                <el-dropdown-item command="subjectivewStruct"
+                  >主观题结构</el-dropdown-item
+                >
+                <el-dropdown-item command="subjectivewGroup"
+                  >主观题分组</el-dropdown-item
+                >
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+
+          <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="fhy">客观题</el-dropdown-item>
+                <el-dropdown-item command="fhy">主观题</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+
+          <el-button type="success" @click="onAnalysis">客观题统计</el-button>
+          <el-button type="primary" @click="onAnalysis">分析统计</el-button>
+          <el-button type="primary" @click="onImportCommand('package')"
+            >导入数据包</el-button
+          >
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+
+  <div class="part-box">
+    <el-table v-loading="loadingTable" :data="dataList" class="page-table">
+      <el-table-column prop="name" label="科目名称">
+        <template #default="{ row }">
+          <el-button type="primary" link @click="onEditPaperStruct(row)">
+            {{ row.code }}-{{ row.name }}
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column prop="level" label="层次" />
+      <el-table-column prop="majorType" label="专业类型" />
+      <el-table-column prop="optional" label="选做科目">
+        <template #default="{ row }">
+          {{ row.optional ? '是' : '否' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="paperUrl" label="试卷">
+        <template #default="{ row }">
+          <el-link
+            v-if="row.paperUrl"
+            type="primary"
+            :href="row.paperUrl"
+            target="_blank"
+          >
+            已上传
+          </el-link>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="answerUrl" label="答案">
+        <template #default="{ row }">
+          <el-link
+            v-if="row.answerUrl"
+            type="primary"
+            :href="row.answerUrl"
+            target="_blank"
+          >
+            已上传
+          </el-link>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="paperType" label="试卷类型" />
+      <el-table-column prop="cardFormatType" label="卡格式" />
+      <el-table-column prop="objectiveTotalScore" label="客观总分" />
+      <el-table-column prop="subjectiveTotalScore" label="主观总分" />
+      <el-table-column prop="paperTotalScore" label="试卷总分" />
+      <el-table-column prop="status" label="状态">
+        <template #default="{ row }">
+          <el-tag :type="row.status === '正常' ? 'success' : 'danger'">
+            {{ row.status }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="200">
+        <template #default="{ row }">
+          <el-button size="small" type="primary" link @click="onEdit(row)">
+            编辑
+          </el-button>
+          <el-button size="small" type="primary" link @click="onAnalysis(row)">
+            分析计算
+          </el-button>
+          <el-button
+            size="small"
+            type="primary"
+            link
+            @click="onSetOptional(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>
+
+  <!-- 科目设置弹窗 -->
+  <SubjectSettingDialog
+    ref="subjectSettingDialogRef"
+    :subject-data="currentSubject"
+    @modified="getList"
+  />
+
+  <!-- 导入 -->
+  <ImportDialog
+    ref="importDialogRef"
+    :title="importConfig?.title"
+    :upload-url="importConfig?.url"
+    :format="importConfig?.format"
+    :download-handle="downloadTemplate"
+    :download-filename="importConfig?.donloadFilename"
+  />
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref, onMounted, computed } from 'vue';
+  import { useRouter } from 'vue-router';
+  import { getSubjectList, getSubjectTotalScoreStatList } from '@/api/subject';
+  import type {
+    SubjectItem,
+    SubjectListFilter,
+    SubjectTotalScoreStatItem,
+  } from '@/api/types/subject';
+  import useTable from '@/hooks/table';
+
+  import SubjectSettingDialog from './components/SubjectSettingDialog.vue';
+
+  defineOptions({
+    name: 'SubjectManage',
+  });
+
+  const router = useRouter();
+
+  const searchModel = reactive<SubjectListFilter>({
+    subject: null,
+    level: '',
+    majorType: '',
+    optional: undefined,
+    status: '',
+    totalScore: undefined,
+  });
+
+  const {
+    dataList,
+    pagination,
+    getList,
+    toPage,
+    pageSizeChange,
+    loading: loadingTable,
+  } = useTable<SubjectItem>(getSubjectList, searchModel, false);
+
+  const subjectSettingDialogRef = ref<InstanceType<
+    typeof SubjectSettingDialog
+  > | null>(null);
+  const currentSubject = ref<SubjectItem | undefined>(undefined);
+  const chartRef = ref();
+  const chartOption = ref({});
+
+  // 更新图表
+  const updateChart = (data: SubjectTotalScoreStatItem[]) => {
+    chartOption.value = {
+      title: {
+        text: '科目总分统计',
+        left: 'center',
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'shadow',
+        },
+      },
+      xAxis: {
+        type: 'category',
+        data: data.map((item) => item.totalScore),
+      },
+      yAxis: {
+        type: 'value',
+        name: '科目数量',
+      },
+      series: [
+        {
+          name: '数量',
+          type: 'bar',
+          data: data.map((item) => item.count),
+          itemStyle: {
+            color: '#409EFF',
+          },
+        },
+      ],
+    };
+  };
+
+  // 获取科目总分统计数据
+  const getStatData = async () => {
+    try {
+      // 这里需要传入实际的examId,暂时使用1作为示例
+      const data = await getSubjectTotalScoreStatList(1);
+      updateChart(data);
+    } catch (error) {
+      console.error('获取统计数据失败:', error);
+    }
+  };
+
+  const onEdit = (row: SubjectItem) => {
+    currentSubject.value = row;
+    subjectSettingDialogRef.value?.open();
+  };
+
+  const onEditPaperStruct = (row: SubjectItem) => {
+    router.push({
+      name: 'PaperStructEdit',
+      params: { subjectId: row.id },
+    });
+  };
+
+  const onAnalysis = (row: SubjectItem | undefined) => {
+    currentSubject.value = row;
+  };
+
+  const onSetOptional = (row: SubjectItem) => {
+    currentSubject.value = row;
+  };
+
+  // 导出
+  const onExportCommand = (command: string) => {
+    console.log('导出命令:', command);
+  };
+
+  // 导入
+  const impoartConfigList = ref([
+    {
+      code: 'objective',
+      title: '客观题',
+      url: '/api/admin/subject/import/kgt',
+      format: ['xls', 'xlsx'],
+      donloadFilename: '客观题导入模板.xlsx',
+    },
+    {
+      code: 'zgtjg',
+      title: 'subjectivewStruct',
+      url: '/api/admin/subject/import/zgtjg',
+      format: ['xls', 'xlsx'],
+      donloadFilename: '主观题结构导入模板.xlsx',
+    },
+    {
+      code: 'subjectivewGroup',
+      title: '主观题分组',
+      url: '/api/admin/subject/import/zgtfhy',
+      format: ['xls', 'xlsx'],
+      donloadFilename: '主观题分组导入模板.xlsx',
+    },
+    {
+      code: 'package',
+      title: '数据包',
+      url: '/api/admin/subject/import/sjb',
+      format: ['zip'],
+      donloadFilename: undefined,
+    },
+  ]);
+  const importDialogRef = ref();
+  const importData = reactive({
+    importType: 'objective',
+  });
+  const importConfig = computed(() => {
+    return impoartConfigList.value.find(
+      (item) => item.code === importData.importType
+    );
+  });
+  const onImportCommand = (command: string) => {
+    console.log('导入命令:', command);
+    importData.importType = command;
+    importDialogRef.value?.open();
+  };
+  async function downloadTemplate() {
+    // const res = await downloadByApi(() => agentTemplate()).catch((e) => {
+    //   Message.error(e || '下载失败,请重新尝试!');
+    // });
+    // if (!res) return;
+    // Message.success('下载成功!');
+  }
+
+  onMounted(() => {
+    getList();
+    getStatData();
+  });
+</script>
+
+<style scoped>
+  .chart-container {
+    padding: 20px;
+    background: #fff;
+    border-radius: 4px;
+    margin-bottom: 20px;
+  }
+
+  .chart-container h3 {
+    margin: 0 0 20px 0;
+    font-size: 16px;
+    font-weight: 500;
+    color: #303133;
+  }
+</style>

+ 283 - 0
src/views/subject/components/ModifyPaperStructure.vue

@@ -0,0 +1,283 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    width="600px"
+    :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="formRules"
+      label-width="100px"
+    >
+      <el-form-item label="试卷类型" prop="paperType">
+        <el-select
+          v-model="formModel.paperType"
+          placeholder="请选择试卷类型"
+          style="width: 100%"
+        >
+          <el-option label="A" value="A" />
+          <el-option label="B" value="B" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="大题名称" prop="bigQuestionName">
+        <el-input
+          v-model="formModel.bigQuestionName"
+          placeholder="请输入大题名称"
+          maxlength="50"
+        />
+      </el-form-item>
+
+      <el-form-item label="大题昵称" prop="bigQuestionNickname">
+        <el-input
+          v-model="formModel.bigQuestionNickname"
+          placeholder="请输入大题昵称"
+          maxlength="50"
+        />
+      </el-form-item>
+
+      <el-form-item label="大题号" prop="bigQuestionNo">
+        <el-input-number
+          v-model="formModel.bigQuestionNo"
+          :min="1"
+          :max="99"
+          placeholder="请输入大题号"
+          style="width: 100%"
+        />
+      </el-form-item>
+
+      <el-form-item label="小题号" prop="smallQuestionNo">
+        <el-input-number
+          v-model="formModel.smallQuestionNo"
+          :min="1"
+          :max="999"
+          placeholder="请输入小题号"
+          style="width: 100%"
+        />
+      </el-form-item>
+
+      <el-form-item label="满分" prop="fullScore">
+        <el-input-number
+          v-model="formModel.fullScore"
+          :min="0"
+          :max="999"
+          :precision="1"
+          placeholder="请输入满分"
+          style="width: 100%"
+        />
+      </el-form-item>
+
+      <el-form-item label="给分次数" prop="giveScoreCount">
+        <el-input-number
+          v-model="formModel.giveScoreCount"
+          :min="1"
+          :max="10"
+          placeholder="请输入给分次数"
+          style="width: 100%"
+        />
+      </el-form-item>
+
+      <el-form-item label="是否客观题">
+        <el-checkbox v-model="formModel.isObjective">客观题</el-checkbox>
+      </el-form-item>
+
+      <el-form-item label="间隔分" prop="intervalScore">
+        <el-input-number
+          v-model="formModel.intervalScore"
+          :min="0"
+          :max="10"
+          :precision="1"
+          placeholder="请输入间隔分"
+          style="width: 100%"
+        />
+      </el-form-item>
+
+      <el-form-item label="题型" prop="questionType">
+        <el-select
+          v-model="formModel.questionType"
+          placeholder="请选择题型"
+          style="width: 100%"
+        >
+          <el-option label="单选" value="单选" />
+          <el-option label="多选" value="多选" />
+          <el-option label="填空" value="填空" />
+          <el-option label="简答" value="简答" />
+          <el-option label="计算" value="计算" />
+          <el-option label="作文" value="作文" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="答案" prop="answer">
+        <el-input
+          v-model="formModel.answer"
+          placeholder="请输入答案"
+          maxlength="200"
+        />
+      </el-form-item>
+
+      <el-form-item label="判分策略" prop="scoringStrategy">
+        <el-select
+          v-model="formModel.scoringStrategy"
+          placeholder="请选择判分策略"
+          style="width: 100%"
+        >
+          <el-option label="无" value="无" />
+          <el-option label="全对全错" value="全对全错" />
+          <el-option label="按步给分" value="按步给分" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="分组" prop="grouping">
+        <el-input
+          v-model="formModel.grouping"
+          placeholder="请输入分组信息(可选)"
+          maxlength="10"
+        />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="onClose">取消</el-button>
+        <el-button type="primary" :loading="loading" @click="confirm">
+          确定
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref, computed } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import { savePaperStructure } from '@/api/subject';
+  import type {
+    PaperStructureItem,
+    PaperStructureUpdateParams,
+  } from '@/api/types/subject';
+
+  import useModal from '@/hooks/modal';
+  import useLoading from '@/hooks/loading';
+  import { objAssign, objModifyAssign } from '@/utils/utils';
+
+  defineOptions({
+    name: 'ModifyPaperStructure',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  interface Props {
+    rowData: PaperStructureItem;
+    subjectId: number;
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    rowData: () => ({} as PaperStructureItem),
+    subjectId: 0,
+  });
+
+  const emit = defineEmits(['modified']);
+
+  const isEdit = computed(() => !!props.rowData.id);
+  const title = computed(() => `${isEdit.value ? '编辑' : '新增'}试卷结构`);
+
+  const formRef = ref<FormInstance>();
+  const initialFormState: PaperStructureUpdateParams = {
+    paperType: '',
+    bigQuestionName: '',
+    bigQuestionNickname: '',
+    bigQuestionNo: 1,
+    smallQuestionNo: 1,
+    fullScore: 0,
+    giveScoreCount: 1,
+    isObjective: false,
+    intervalScore: 0.5,
+    questionType: '',
+    answer: '',
+    scoringStrategy: '无',
+    grouping: '',
+  };
+  const formModel = reactive<PaperStructureUpdateParams>({
+    ...initialFormState,
+  });
+
+  const formRules: FormRules<keyof PaperStructureUpdateParams> = {
+    paperType: [
+      { required: true, message: '请选择试卷类型', trigger: 'change' },
+    ],
+    bigQuestionName: [
+      { required: true, message: '请输入大题名称', trigger: 'blur' },
+      { max: 50, message: '大题名称不能超过50个字符', trigger: 'blur' },
+    ],
+    bigQuestionNickname: [
+      { max: 50, message: '大题昵称不能超过50个字符', trigger: 'blur' },
+    ],
+    bigQuestionNo: [
+      { required: true, message: '请输入大题号', trigger: 'blur' },
+    ],
+    smallQuestionNo: [
+      { required: true, message: '请输入小题号', trigger: 'blur' },
+    ],
+    fullScore: [{ required: true, message: '请输入满分', trigger: 'blur' }],
+    giveScoreCount: [
+      { required: true, message: '请输入给分次数', trigger: 'blur' },
+    ],
+    intervalScore: [
+      { required: true, message: '请输入间隔分', trigger: 'blur' },
+    ],
+    questionType: [
+      { required: true, message: '请选择题型', trigger: 'change' },
+    ],
+    answer: [{ max: 200, message: '答案不能超过200个字符', trigger: 'blur' }],
+    scoringStrategy: [
+      { required: true, message: '请选择判分策略', trigger: 'change' },
+    ],
+    grouping: [
+      { max: 10, message: '分组信息不能超过10个字符', trigger: 'blur' },
+    ],
+  };
+
+  const handleClose = () => {
+    formRef.value?.resetFields();
+  };
+
+  const { loading, setLoading } = useLoading();
+  const confirm = async () => {
+    const valid = await formRef.value?.validate().catch(() => false);
+    if (!valid) return;
+
+    setLoading(true);
+    const datas = objAssign(formModel, {});
+    if (isEdit.value) {
+      datas.id = props.rowData.id;
+    }
+    let res = true;
+    await savePaperStructure(datas).catch(() => {
+      res = false;
+    });
+    setLoading(false);
+    if (!res) return;
+    ElMessage.success(`${title.value}成功!`);
+    emit('modified', datas);
+    close();
+  };
+
+  /* init modal */
+  function modalBeforeOpen() {
+    if (props.rowData.id) {
+      objModifyAssign(formModel, props.rowData);
+    } else {
+      objModifyAssign(formModel, initialFormState);
+    }
+  }
+</script>

+ 220 - 0
src/views/subject/components/SubjectSettingDialog.vue

@@ -0,0 +1,220 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    width="800px"
+    :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="formRules"
+      label-width="140px"
+      class="setting-form"
+    >
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="科目名称" prop="name">
+            {{ formModel.name }}
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="科目代码" prop="code">
+            {{ formModel.code }}
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="及格分" prop="passScore">
+            <el-input-number
+              v-model="formModel.passScore"
+              :min="0"
+              :max="1000"
+              :precision="1"
+              style="width: 100%"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="优秀分" prop="excellentScore">
+            <el-input-number
+              v-model="formModel.excellentScore"
+              :min="0"
+              :max="1000"
+              :precision="1"
+              style="width: 100%"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-form-item label="评卷显示题目昵称">
+        <el-checkbox v-model="formModel.showQuestionNickname" />
+      </el-form-item>
+
+      <el-form-item label="评卷提交自动定位">
+        <el-radio-group v-model="formModel.autoLocate">
+          <el-radio :value="3">设置以考试为准</el-radio>
+          <el-radio :value="6">是</el-radio>
+          <el-radio :value="9">否</el-radio>
+        </el-radio-group>
+        <p>
+          *开启后,当前试卷评完后,下份试卷自动定位到上份试卷的第一个轨迹点
+        </p>
+      </el-form-item>
+
+      <el-form-item label="自动对切题卡">
+        <el-radio-group v-model="formModel.autoCut">
+          <el-radio :value="3">设置以考试为准</el-radio>
+          <el-radio :value="6">是</el-radio>
+          <el-radio :value="9">否</el-radio>
+        </el-radio-group>
+        <p>
+          *选持自动对切题卡后,在阅卷端自动按照题卡的55%的比例进行对切,纸张大小A4时不生效
+        </p>
+      </el-form-item>
+
+      <el-form-item label="原图遮盖">
+        <el-button type="primary" size="small" @click="addCoverArea">
+          设置
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="close">取消</el-button>
+        <el-button type="primary" :loading="loading" @click="confirm">
+          保存
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+
+  <!-- TODO: 添加遮盖区域弹窗 -->
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref, computed } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import { saveSubjectSetting, getSubjectSetting } from '@/api/subject';
+  import type { SubjectItem, SubjectSettingInfo } from '@/api/types/subject';
+
+  import useModal from '@/hooks/modal';
+  import useLoading from '@/hooks/loading';
+  import { objAssign, objModifyAssign } from '@/utils/utils';
+
+  defineOptions({
+    name: 'SubjectSettingDialog',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  interface Props {
+    rowData?: SubjectItem;
+  }
+
+  const props = defineProps<Props>();
+
+  const emit = defineEmits(['modified']);
+
+  const title = computed(() => '科目设置');
+
+  const formRef = ref<FormInstance>();
+
+  const initialFormState: SubjectSettingInfo = {
+    id: 0,
+    name: '',
+    code: '',
+    showQuestionNickname: false,
+    passScore: 60,
+    excellentScore: 85,
+    sheetConfig: null,
+    autoLocate: false,
+    autoCut: false,
+  };
+
+  const formModel = reactive<SubjectSettingInfo>({
+    ...initialFormState,
+  });
+
+  const formRules: FormRules<keyof SubjectSettingInfo> = {
+    passScore: [
+      { required: true, message: '请输入及格分', trigger: 'blur' },
+      {
+        type: 'number',
+        min: 0,
+        max: 1000,
+        message: '及格分必须在0-1000之间',
+        trigger: 'blur',
+      },
+    ],
+    excellentScore: [
+      { required: true, message: '请输入优秀分', trigger: 'blur' },
+      {
+        type: 'number',
+        min: 0,
+        max: 1000,
+        message: '优秀分必须在0-1000之间',
+        trigger: 'blur',
+      },
+    ],
+  };
+
+  const handleClose = () => {
+    formRef.value?.resetFields();
+  };
+
+  // 添加遮盖区域
+  const addCoverArea = () => {};
+
+  // 提交
+  const { loading, setLoading } = useLoading();
+  const confirm = async () => {
+    const valid = await formRef.value?.validate().catch(() => false);
+    if (!valid) return;
+
+    setLoading(true);
+    const datas = objAssign(formModel, {});
+    let res = true;
+    await saveSubjectSetting(datas).catch(() => {
+      res = false;
+    });
+    setLoading(false);
+    if (!res) return;
+    ElMessage.success('保存成功!');
+    emit('modified', datas);
+    close();
+  };
+
+  /* init modal */
+  async function modalBeforeOpen() {
+    if (props.rowData) {
+      try {
+        // 获取科目设置信息
+        const settingData = await getSubjectSetting(props.rowData.id);
+        objModifyAssign(formModel, settingData);
+      } catch (error) {
+        // 如果获取失败,使用默认值
+        objModifyAssign(formModel, {
+          ...initialFormState,
+          id: props.rowData.id,
+          name: props.rowData.name,
+          code: props.rowData.code,
+        });
+      }
+    } else {
+      objModifyAssign(formModel, initialFormState);
+    }
+  }
+</script>