Browse Source

feat: 科目分析

zhangjie 1 tuần trước cách đây
mục cha
commit
3027b47159

+ 73 - 0
src/api/analysis.ts

@@ -0,0 +1,73 @@
+import axios from 'axios';
+
+import {
+  ScoreSegmentListRes,
+  TotalAnalysisListRes,
+  CollegeAnalysisListParam,
+  CollegeAnalysisListRes,
+  TeacherAnalysisListParam,
+  TeacherAnalysisListRes,
+  ClassAnalysisListParam,
+  ClassAnalysisListRes,
+  QuestionAnalysisListParam,
+  QuestionAnalysisListRes,
+  BigQuestionAnalysisListRes,
+  ScoreSegmentListParam,
+  AnalysisListPageParam,
+} from './types/analysis';
+
+// 总量分析列表
+export function getTotalAnalysisList(
+  params: AnalysisListPageParam
+): Promise<TotalAnalysisListRes> {
+  return axios.post('/api/analysis/total', { params });
+}
+
+// 分段统计列表
+export function getSegmentAnalysisList(
+  params: ScoreSegmentListParam
+): Promise<ScoreSegmentListRes> {
+  return axios.post('/api/analysis/segment', { params });
+}
+
+// 学院分析列表
+export function getCollegeAnalysisList(
+  params: CollegeAnalysisListParam
+): Promise<CollegeAnalysisListRes> {
+  return axios.post('/api/analysis/college', { params });
+}
+
+// 任课老师分析列表
+export function getTeacherAnalysisList(
+  params: TeacherAnalysisListParam
+): Promise<TeacherAnalysisListRes> {
+  return axios.post('/api/analysis/teacher', { params });
+}
+
+// 班级分析列表
+export function getClassAnalysisList(
+  params: ClassAnalysisListParam
+): Promise<ClassAnalysisListRes> {
+  return axios.post('/api/analysis/class', { params });
+}
+
+// 客观题分析列表
+export function getObjectiveQuestionAnalysisList(
+  params: QuestionAnalysisListParam
+): Promise<QuestionAnalysisListRes> {
+  return axios.post('/api/analysis/question', { params });
+}
+
+// 主观题分析列表
+export function getSubjectiveQuestionAnalysisList(
+  params: QuestionAnalysisListParam
+): Promise<QuestionAnalysisListRes> {
+  return axios.post('/api/analysis/question', { params });
+}
+
+// 大题分析列表
+export function getBigQuestionAnalysisList(
+  params: AnalysisListPageParam
+): Promise<BigQuestionAnalysisListRes> {
+  return axios.post('/api/analysis/big-question', { params });
+}

+ 214 - 0
src/api/types/analysis.ts

@@ -0,0 +1,214 @@
+import { PageResult, PageParams } from './common';
+
+export interface AnalysisListFilter {
+  // 科目
+  subject: number | null;
+}
+export type AnalysisListPageParam = PageParams<AnalysisListFilter>;
+
+// 总量分析项目
+export interface TotalAnalysisItem {
+  // 科目名称
+  subject: string;
+  // 报考人数
+  totalStudents: number;
+  // 缺考人数
+  absentStudents: number;
+  // 违纪人数
+  violationStudents: number;
+  // 有效考试人数
+  validStudents: number;
+  // 平均分
+  averageScore: number;
+  // 最高分
+  highestScore: number;
+  // 最低分
+  lowestScore: number;
+  // 及格人数
+  passStudents: number;
+  // 及格率
+  passRate: number;
+  // 优秀人数
+  excellentStudents: number;
+  // 优秀率
+  excellentRate: number;
+}
+export type TotalAnalysisListRes = PageResult<TotalAnalysisItem>;
+
+// 分段统计项目
+export interface ScoreSegmentItem {
+  // 分数段
+  segment: string;
+  // 人数
+  students: number;
+  // 频率(%)
+  rate: number;
+}
+export type ScoreSegmentListRes = PageResult<ScoreSegmentItem>;
+
+export interface ScoreSegmentFilter {
+  // 科目
+  subject: string;
+  // 分数间隔类型
+  intervalType: string;
+}
+export type ScoreSegmentListParam = PageParams<ScoreSegmentFilter>;
+
+// 学院分析
+export interface CollegeAnalysisItem {
+  // 学生院系
+  college: string;
+  // 报考人数
+  totalStudents: number;
+  // 有效人数
+  validStudents: number;
+  // 平均分
+  averageScore: number;
+  // 最高分
+  highestScore: number;
+  // 最低分
+  lowestScore: number;
+  // 及格人数
+  passStudents: number;
+  // 及格率
+  passRate: number;
+  // 优秀人数
+  excellentStudents: number;
+  // 优秀率
+  excellentRate: number;
+}
+export type CollegeAnalysisListRes = PageResult<CollegeAnalysisItem>;
+
+export interface CollegeAnalysisFilter {
+  // 科目
+  subject: string;
+  // 学生院系
+  college: string;
+}
+export type CollegeAnalysisListParam = PageParams<CollegeAnalysisFilter>;
+
+// 任课老师分析
+export interface TeacherAnalysisItem {
+  // 任课老师
+  teacher: string;
+  // 报考人数
+  totalStudents: number;
+  // 有效人数
+  validStudents: number;
+  // 及格人数
+  passStudents: number;
+  // 优秀人数
+  excellentStudents: number;
+  // 最高分
+  highestScore: number;
+  // 最低分
+  lowestScore: number;
+  // 及格率
+  passRate: number;
+  // 优秀率
+  excellentRate: number;
+  // 平均分
+  averageScore: number;
+  // 平均相对分
+  averageRelativeScore: number;
+}
+export type TeacherAnalysisListRes = PageResult<TeacherAnalysisItem>;
+
+export interface TeacherAnalysisFilter {
+  // 科目
+  subject: string;
+  // 任课老师
+  teacher: string;
+}
+export type TeacherAnalysisListParam = PageParams<TeacherAnalysisFilter>;
+
+// 班级分析项目
+export interface ClassAnalysisItem {
+  // 班级
+  className: string;
+  // 报考人数
+  totalStudents: number;
+  // 有效人数
+  validStudents: number;
+  // 平均分
+  averageScore: number;
+  // 最高分
+  highestScore: number;
+  // 最低分
+  lowestScore: number;
+  // 及格人数
+  passStudents: number;
+  // 及格率
+  passRate: number;
+  // 优秀人数
+  excellentStudents: number;
+  // 优秀率
+  excellentRate: number;
+}
+export type ClassAnalysisListRes = PageResult<ClassAnalysisItem>;
+
+export interface ClassAnalysisFilter {
+  // 科目
+  subject: string;
+  // 班级
+  className: string;
+}
+export type ClassAnalysisListParam = PageParams<ClassAnalysisFilter>;
+
+// 主、客观题分析项目
+export interface QuestionAnalysisItem {
+  // 题目名称
+  questionName: string;
+  // 大题号
+  bigQuestionNo: number;
+  // 小题号
+  smallQuestionNo: number;
+  // 单题分数
+  singleQuestionScore: number;
+  // 单题平均分
+  singleQuestionAverageScore: number;
+  // 单题标准差
+  singleQuestionStandardDeviation: number;
+  // 得分率
+  scoreRate: number;
+  // 满分率
+  fullScoreRate: number;
+  // 试卷类型
+  paperType?: string;
+}
+export type QuestionAnalysisListRes = PageResult<QuestionAnalysisItem>;
+
+export interface QuestionAnalysisFilter {
+  // 科目
+  subject: string;
+  // 试卷类型
+  paperType?: string;
+}
+export type QuestionAnalysisListParam = PageParams<QuestionAnalysisFilter>;
+
+// 大题分析项目
+export interface BigQuestionAnalysisItem {
+  // 大题名称
+  bigQuestionName: string;
+  // 大题号
+  bigQuestionNo: number;
+  // 满分
+  totalScore: number;
+  // 最高分
+  highestScore: number;
+  // 最低分
+  lowestScore: number;
+  // 平均分
+  averageScore: number;
+  // 标准差
+  standardDeviation: number;
+  // 差异系数
+  differenceCoefficient: number;
+  // 得分率
+  scoreRate: number;
+  // 零分人数
+  zeroScoreStudents: number;
+  // 满分人数
+  fullScoreStudents: number;
+}
+export type BigQuestionAnalysisListRes = PageResult<BigQuestionAnalysisItem>;

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

@@ -36,6 +36,15 @@ const BASE: AppRouteRecordRaw = {
         requiresAuth: true,
         requiresAuth: true,
       },
       },
     },
     },
+    {
+      path: '/analysis-manage',
+      name: 'AnalysisManage',
+      component: () => import('@/views/analysis/AnalysisManage.vue'),
+      meta: {
+        title: '科目分析',
+        requiresAuth: true,
+      },
+    },
     {
     {
       path: '/log-manage',
       path: '/log-manage',
       name: 'LogManage',
       name: 'LogManage',

+ 1 - 1
src/store/modules/app/menuData.ts

@@ -195,7 +195,7 @@ export const adminMenus = [
   {
   {
     id: 19,
     id: 19,
     name: '科目分析',
     name: '科目分析',
-    url: 'SubjectAnalysis',
+    url: 'AnalysisManage',
     type: 'MENU',
     type: 'MENU',
     parentId: -1,
     parentId: -1,
     sequence: 2,
     sequence: 2,

+ 50 - 0
src/views/analysis/AnalysisManage.vue

@@ -0,0 +1,50 @@
+<template>
+  <el-tabs v-model="activeTab" type="card" class="page-tab">
+    <el-tab-pane label="总量分析" name="total">
+      <TotalAnalysis />
+    </el-tab-pane>
+    <el-tab-pane label="分段统计" name="segment">
+      <SegmentAnalysis />
+    </el-tab-pane>
+    <el-tab-pane label="学院分析" name="college">
+      <CollegeAnalysis />
+    </el-tab-pane>
+    <el-tab-pane label="任课老师分析" name="teacher">
+      <TeacherAnalysis />
+    </el-tab-pane>
+    <el-tab-pane label="班级分析" name="class">
+      <ClassAnalysis />
+    </el-tab-pane>
+    <el-tab-pane label="客观题分析" name="objective">
+      <ObjectiveQuestionAnalysis />
+    </el-tab-pane>
+    <el-tab-pane label="主观题分析" name="subjective">
+      <SubjectiveQuestionAnalysis />
+    </el-tab-pane>
+    <el-tab-pane label="大题分析" name="bigQuestion">
+      <BigQuestionAnalysis />
+    </el-tab-pane>
+  </el-tabs>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { useRoute } from 'vue-router';
+
+  import TotalAnalysis from './TotalAnalysis.vue';
+  import SegmentAnalysis from './SegmentAnalysis.vue';
+  import CollegeAnalysis from './CollegeAnalysis.vue';
+  import TeacherAnalysis from './TeacherAnalysis.vue';
+  import ClassAnalysis from './ClassAnalysis.vue';
+  import ObjectiveQuestionAnalysis from './ObjectiveQuestionAnalysis.vue';
+  import SubjectiveQuestionAnalysis from './SubjectiveQuestionAnalysis.vue';
+  import BigQuestionAnalysis from './BigQuestionAnalysis.vue';
+
+  defineOptions({
+    name: 'AnalysisManage',
+  });
+
+  const route = useRoute();
+  // 从路由参数获取tab值,默认为total
+  const activeTab = ref((route.query.tab as string) || 'total');
+</script>

+ 105 - 0
src/views/analysis/BigQuestionAnalysis.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">导出</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column
+        property="bigQuestionName"
+        label="大题名称"
+        min-width="150"
+      />
+      <el-table-column property="bigQuestionNo" label="大题号" min-width="80" />
+      <el-table-column property="totalScore" label="满分" min-width="80" />
+      <el-table-column property="highestScore" label="最高分" min-width="100" />
+      <el-table-column property="lowestScore" label="最低分" min-width="100" />
+      <el-table-column property="averageScore" label="平均分" min-width="100">
+        <template #default="scope">
+          {{ scope.row.averageScore?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="standardDeviation"
+        label="标准差"
+        min-width="100"
+      >
+        <template #default="scope">
+          {{ scope.row.standardDeviation?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="differenceCoefficient"
+        label="差异系数"
+        min-width="100"
+      >
+        <template #default="scope">
+          {{ scope.row.differenceCoefficient?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="得分率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.scoreRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="zeroScoreStudents"
+        label="零分人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="fullScoreStudents"
+        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 } from 'vue';
+  import { getBigQuestionAnalysisList } from '@/api/analysis';
+  import {
+    BigQuestionAnalysisItem,
+    AnalysisListFilter,
+  } from '@/api/types/analysis';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'BigQuestionAnalysis',
+  });
+
+  const searchModel = reactive<AnalysisListFilter>({
+    subject: null,
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<BigQuestionAnalysisItem>(
+      getBigQuestionAnalysisList,
+      searchModel,
+      false
+    );
+
+  function exportData() {
+    // TODO: 实现导出功能
+    console.log('导出大题分析数据');
+  }
+</script>

+ 98 - 0
src/views/analysis/ClassAnalysis.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item label="班级">
+        <el-input
+          v-model="searchModel.className"
+          placeholder="请输入班级名称"
+          clearable
+          style="width: 200px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">导出</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column property="className" label="班级" min-width="150" />
+      <el-table-column
+        property="totalStudents"
+        label="报考人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="validStudents"
+        label="有效人数"
+        min-width="100"
+      />
+      <el-table-column property="averageScore" label="平均分" min-width="100">
+        <template #default="scope">
+          {{ scope.row.averageScore?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column property="highestScore" label="最高分" min-width="100" />
+      <el-table-column property="lowestScore" label="最低分" min-width="100" />
+      <el-table-column
+        property="passStudents"
+        label="及格人数"
+        min-width="100"
+      />
+      <el-table-column label="及格率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.passRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="excellentStudents"
+        label="优秀人数"
+        min-width="100"
+      />
+      <el-table-column label="优秀率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.excellentRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      v-model:current-page="pagination.pageNumber"
+      v-model:page-size="pagination.pageSize"
+      :layout="pagination.layout"
+      :total="pagination.total"
+      @size-change="pageSizeChange"
+      @current-change="toPage"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive } from 'vue';
+  import { getClassAnalysisList } from '@/api/analysis';
+  import { ClassAnalysisItem, ClassAnalysisFilter } from '@/api/types/analysis';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'ClassAnalysis',
+  });
+
+  const searchModel = reactive<ClassAnalysisFilter>({
+    subject: null,
+    className: '',
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<ClassAnalysisItem>(getClassAnalysisList, searchModel, false);
+
+  function exportData() {
+    // TODO: 实现导出功能
+    console.log('导出班级分析数据');
+  }
+</script>

+ 101 - 0
src/views/analysis/CollegeAnalysis.vue

@@ -0,0 +1,101 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item label="学生院系">
+        <el-input
+          v-model="searchModel.college"
+          placeholder="请输入学院名称"
+          clearable
+          style="width: 200px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">导出</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column property="college" label="学生院系" min-width="150" />
+      <el-table-column
+        property="totalStudents"
+        label="报考人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="validStudents"
+        label="有效人数"
+        min-width="100"
+      />
+      <el-table-column property="averageScore" label="平均分" min-width="100">
+        <template #default="scope">
+          {{ scope.row.averageScore?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column property="highestScore" label="最高分" min-width="100" />
+      <el-table-column property="lowestScore" label="最低分" min-width="100" />
+      <el-table-column
+        property="passStudents"
+        label="及格人数"
+        min-width="100"
+      />
+      <el-table-column label="及格率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.passRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="excellentStudents"
+        label="优秀人数"
+        min-width="100"
+      />
+      <el-table-column label="优秀率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.excellentRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      v-model:current-page="pagination.pageNumber"
+      v-model:page-size="pagination.pageSize"
+      :layout="pagination.layout"
+      :total="pagination.total"
+      @size-change="pageSizeChange"
+      @current-change="toPage"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive } from 'vue';
+  import { getCollegeAnalysisList } from '@/api/analysis';
+  import {
+    CollegeAnalysisItem,
+    CollegeAnalysisFilter,
+  } from '@/api/types/analysis';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'CollegeAnalysis',
+  });
+
+  const searchModel = reactive<CollegeAnalysisFilter>({
+    subject: null,
+    college: '',
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<CollegeAnalysisItem>(getCollegeAnalysisList, searchModel, false);
+
+  function exportData() {
+    // TODO: 实现导出功能
+    console.log('导出学院分析数据');
+  }
+</script>

+ 116 - 0
src/views/analysis/ObjectiveQuestionAnalysis.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item label="试卷类型">
+        <el-select
+          v-model="searchModel.paperType"
+          placeholder="请选择"
+          clearable
+          style="width: 150px"
+        >
+          <el-option label="A卷" value="A" />
+          <el-option label="B卷" value="B" />
+          <el-option label="C卷" value="C" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">导出</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column
+        property="questionName"
+        label="题目名称"
+        min-width="150"
+      />
+      <el-table-column property="bigQuestionNo" label="大题号" min-width="80" />
+      <el-table-column
+        property="smallQuestionNo"
+        label="小题号"
+        min-width="80"
+      />
+      <el-table-column
+        property="singleQuestionScore"
+        label="单题分数"
+        min-width="100"
+      />
+      <el-table-column
+        property="singleQuestionAverageScore"
+        label="单题平均分"
+        min-width="120"
+      >
+        <template #default="scope">
+          {{ scope.row.singleQuestionAverageScore?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="singleQuestionStandardDeviation"
+        label="单题标准差"
+        min-width="120"
+      >
+        <template #default="scope">
+          {{ scope.row.singleQuestionStandardDeviation?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="得分率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.scoreRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+      <el-table-column label="满分率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.fullScoreRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+      <el-table-column property="paperType" 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 } from 'vue';
+  import { getObjectiveQuestionAnalysisList } from '@/api/analysis';
+  import {
+    QuestionAnalysisItem,
+    QuestionAnalysisFilter,
+  } from '@/api/types/analysis';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'ObjectiveQuestionAnalysis',
+  });
+
+  const searchModel = reactive<QuestionAnalysisFilter>({
+    subject: null,
+    paperType: '',
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<QuestionAnalysisItem>(
+      getObjectiveQuestionAnalysisList,
+      searchModel,
+      false
+    );
+
+  function exportData() {
+    // TODO: 实现导出功能
+    console.log('导出客观题分析数据');
+  }
+</script>

+ 85 - 0
src/views/analysis/SegmentAnalysis.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item label="分数间隔类型">
+        <el-select
+          v-model="searchModel.intervalType"
+          placeholder="请选择"
+          clearable
+          style="width: 150px"
+        >
+          <el-option label="5分间隔" value="5" />
+          <el-option label="10分间隔" value="10" />
+          <el-option label="20分间隔" value="20" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">导出</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column property="segment" label="分数段" min-width="120" />
+      <el-table-column property="students" label="人数" min-width="100" />
+      <el-table-column label="频率" min-width="100">
+        <template #default="scope"> {{ scope.row.rate.toFixed(2) }}% </template>
+      </el-table-column>
+      <el-table-column label="分布图" min-width="200">
+        <template #default="scope">
+          <el-progress
+            :percentage="scope.row.rate"
+            :color="getSegmentColor(scope.row.rate)"
+            :show-text="false"
+          />
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      v-model:current-page="pagination.pageNumber"
+      v-model:page-size="pagination.pageSize"
+      :layout="pagination.layout"
+      :total="pagination.total"
+      @size-change="pageSizeChange"
+      @current-change="toPage"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive } from 'vue';
+  import { getSegmentAnalysisList } from '@/api/analysis';
+  import { ScoreSegmentItem, ScoreSegmentFilter } from '@/api/types/analysis';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'SegmentAnalysis',
+  });
+
+  const searchModel = reactive<ScoreSegmentFilter>({
+    subject: null,
+    intervalType: '10',
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<ScoreSegmentItem>(getSegmentAnalysisList, searchModel, false);
+
+  function getSegmentColor(rate: number) {
+    if (rate < 5) return '#f56c6c';
+    if (rate < 15) return '#e6a23c';
+    if (rate < 25) return '#409eff';
+    return '#67c23a';
+  }
+
+  function exportData() {
+    // TODO: 实现导出功能
+    console.log('导出分段统计数据');
+  }
+</script>

+ 103 - 0
src/views/analysis/SubjectiveQuestionAnalysis.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">导出</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column
+        property="questionName"
+        label="题目名称"
+        min-width="150"
+      />
+      <el-table-column property="bigQuestionNo" label="大题号" min-width="80" />
+      <el-table-column
+        property="smallQuestionNo"
+        label="小题号"
+        min-width="80"
+      />
+      <el-table-column
+        property="singleQuestionScore"
+        label="单题分数"
+        min-width="100"
+      />
+      <el-table-column
+        property="singleQuestionAverageScore"
+        label="单题平均分"
+        min-width="120"
+      >
+        <template #default="scope">
+          {{ scope.row.singleQuestionAverageScore?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="singleQuestionStandardDeviation"
+        label="单题标准差"
+        min-width="120"
+      >
+        <template #default="scope">
+          {{ scope.row.singleQuestionStandardDeviation?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="得分率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.scoreRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+      <el-table-column label="满分率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.fullScoreRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+      <el-table-column property="paperType" 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 } from 'vue';
+  import { getSubjectiveQuestionAnalysisList } from '@/api/analysis';
+  import {
+    QuestionAnalysisItem,
+    QuestionAnalysisFilter,
+  } from '@/api/types/analysis';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'SubjectiveQuestionAnalysis',
+  });
+
+  const searchModel = reactive<QuestionAnalysisFilter>({
+    subject: null,
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<QuestionAnalysisItem>(
+      getSubjectiveQuestionAnalysisList,
+      searchModel,
+      false
+    );
+
+  function exportData() {
+    // TODO: 实现导出功能
+    console.log('导出主观题分析数据');
+  }
+</script>

+ 110 - 0
src/views/analysis/TeacherAnalysis.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item label="任课老师">
+        <el-input
+          v-model="searchModel.teacher"
+          placeholder="请输入老师姓名"
+          clearable
+          style="width: 200px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">导出</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column property="teacher" label="任课老师" min-width="120" />
+      <el-table-column
+        property="totalStudents"
+        label="报考人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="validStudents"
+        label="有效人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="passStudents"
+        label="及格人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="excellentStudents"
+        label="优秀人数"
+        min-width="100"
+      />
+      <el-table-column property="highestScore" label="最高分" min-width="100" />
+      <el-table-column property="lowestScore" label="最低分" min-width="100" />
+      <el-table-column label="及格率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.passRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+      <el-table-column label="优秀率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.excellentRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+      <el-table-column property="averageScore" label="平均分" min-width="100">
+        <template #default="scope">
+          {{ scope.row.averageScore?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="averageRelativeScore"
+        label="平均相对分"
+        min-width="120"
+      >
+        <template #default="scope">
+          {{ scope.row.averageRelativeScore?.toFixed(2) }}
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      v-model:current-page="pagination.pageNumber"
+      v-model:page-size="pagination.pageSize"
+      :layout="pagination.layout"
+      :total="pagination.total"
+      @size-change="pageSizeChange"
+      @current-change="toPage"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive } from 'vue';
+  import { getTeacherAnalysisList } from '@/api/analysis';
+  import {
+    TeacherAnalysisItem,
+    TeacherAnalysisFilter,
+  } from '@/api/types/analysis';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'TeacherAnalysis',
+  });
+
+  const searchModel = reactive<TeacherAnalysisFilter>({
+    subject: null,
+    teacher: '',
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<TeacherAnalysisItem>(getTeacherAnalysisList, searchModel, false);
+
+  function exportData() {
+    // TODO: 实现导出功能
+    console.log('导出任课老师分析数据');
+  }
+</script>

+ 99 - 0
src/views/analysis/TotalAnalysis.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="科目">
+        <select-subject v-model="searchModel.subject"></select-subject>
+      </el-form-item>
+      <el-form-item>
+        <el-space wrap>
+          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button @click="exportData">导出</el-button>
+        </el-space>
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column property="subject" label="科目名称" min-width="120" />
+      <el-table-column
+        property="totalStudents"
+        label="报考人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="absentStudents"
+        label="缺考人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="violationStudents"
+        label="违纪人数"
+        min-width="100"
+      />
+      <el-table-column
+        property="validStudents"
+        label="有效考试人数"
+        min-width="120"
+      />
+      <el-table-column property="averageScore" label="平均分" min-width="100">
+        <template #default="scope">
+          {{ scope.row.averageScore?.toFixed(2) }}
+        </template>
+      </el-table-column>
+      <el-table-column property="highestScore" label="最高分" min-width="100" />
+      <el-table-column property="lowestScore" label="最低分" min-width="100" />
+      <el-table-column
+        property="passStudents"
+        label="及格人数"
+        min-width="100"
+      />
+      <el-table-column label="及格率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.passRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+      <el-table-column
+        property="excellentStudents"
+        label="优秀人数"
+        min-width="100"
+      />
+      <el-table-column label="优秀率" min-width="100">
+        <template #default="scope">
+          {{ (scope.row.excellentRate * 100).toFixed(2) }}%
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      v-model:current-page="pagination.pageNumber"
+      v-model:page-size="pagination.pageSize"
+      :layout="pagination.layout"
+      :total="pagination.total"
+      @size-change="pageSizeChange"
+      @current-change="toPage"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive } from 'vue';
+  import { getTotalAnalysisList } from '@/api/analysis';
+  import { TotalAnalysisItem, AnalysisListFilter } from '@/api/types/analysis';
+  import useTable from '@/hooks/table';
+
+  defineOptions({
+    name: 'TotalAnalysis',
+  });
+
+  const searchModel = reactive<AnalysisListFilter>({
+    subject: null,
+  });
+
+  const { dataList, pagination, loading, toPage, pageSizeChange } =
+    useTable<TotalAnalysisItem>(getTotalAnalysisList, searchModel, false);
+
+  function exportData() {
+    // TODO: 实现导出功能
+    console.log('导出总量分析数据');
+  }
+</script>