Selaa lähdekoodia

feat: 科目分析图表

zhangjie 2 päivää sitten
vanhempi
commit
95fc892820

+ 1 - 0
components.d.ts

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

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

@@ -39,6 +39,8 @@ export type TotalAnalysisListRes = PageResult<TotalAnalysisItem>;
 
 // 分段统计项目
 export interface ScoreSegmentItem {
+  // 分数
+  score: number;
   // 分数段
   range: string;
   // 人数

+ 4 - 1
src/api/types/exam.ts

@@ -97,7 +97,10 @@ export interface ExamListFilter {
 export type ExamListPageParam = PageParams<ExamListFilter>;
 
 // 基础配置
-export type ExamUpdateParam = Omit<ExamItem, 'id' | 'createTime'> & {
+export type ExamUpdateParam = Omit<
+  ExamItem,
+  'id' | 'createTime' | 'examTime'
+> & {
   id?: number;
 };
 

+ 51 - 0
src/components/chart/ChartModal.vue

@@ -0,0 +1,51 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    :width="width"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    top="10vh"
+    append-to-body
+    model-class="chart-modal"
+  >
+    <Chart
+      :options="props.chartOptions"
+      :height="props.chartHeight"
+      :auto-resize="true"
+    />
+
+    <template #footer> </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import type { EChartsOption } from 'echarts';
+  import useModal from '@/hooks/modal';
+  import Chart from './index.vue';
+
+  defineOptions({
+    name: 'ChartModal',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  interface Props {
+    title?: string;
+    chartOptions: EChartsOption;
+    width?: string;
+    chartHeight?: string;
+    showFullscreenBtn?: boolean;
+    showExportBtn?: boolean;
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    title: '图表详情',
+    width: '80%',
+    chartHeight: '500px',
+    showFullscreenBtn: true,
+    showExportBtn: true,
+  });
+</script>

+ 2 - 0
src/utils/echarts.ts

@@ -7,6 +7,7 @@ import {
   LegendComponent,
   DataZoomComponent,
   GraphicComponent,
+  ToolboxComponent,
 } from 'echarts/components';
 
 // Manually introduce ECharts modules to reduce packing size
@@ -21,4 +22,5 @@ use([
   LegendComponent,
   DataZoomComponent,
   GraphicComponent,
+  ToolboxComponent,
 ]);

+ 147 - 3
src/views/analysis/BigQuestionAnalysis.vue

@@ -6,13 +6,14 @@
           v-model="searchModel.subjectCode"
           :clearable="false"
           default-first
-          @default-selected="toPage(1)"
+          @default-selected="search"
         ></select-subject>
       </el-form-item>
       <el-form-item>
         <el-space wrap>
-          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button type="primary" @click="search">查询</el-button>
           <el-button @click="onExport">导出</el-button>
+          <el-button @click="onChart">查看统计图</el-button>
         </el-space>
       </el-form-item>
     </el-form>
@@ -63,10 +64,16 @@
       @current-change="toPage"
     />
   </div>
+
+  <ChartModal
+    ref="chartModalRef"
+    title="客观题分析"
+    :chart-options="chartOptions"
+  />
 </template>
 
 <script setup lang="ts">
-  import { reactive } from 'vue';
+  import { reactive, ref } from 'vue';
   import { getBigQuestionAnalysisList } from '@/api/analysis';
   import {
     BigQuestionAnalysisItem,
@@ -74,6 +81,8 @@
   } from '@/api/types/analysis';
   import useTable from '@/hooks/table';
   import { downloadExport } from '@/utils/download-export';
+  import ChartModal from '@/components/chart/ChartModal.vue';
+  import type { EChartsOption } from 'echarts';
 
   defineOptions({
     name: 'BigQuestionAnalysis',
@@ -89,8 +98,143 @@
       searchModel,
       false
     );
+  async function search() {
+    await toPage(1);
+    updateChartOption();
+  }
 
   async function onExport() {
     await downloadExport('exportBigQuestionAnalysisList', searchModel);
   }
+
+  // 图表
+  const chartOptions = ref<EChartsOption>();
+  function updateChartOption() {
+    const names = dataList.value.map((item) => item.className);
+    const totalScores = dataList.value.map((item) => item.totalScore || 0);
+    const maxScores = dataList.value.map((item) => item.maxScore || 0);
+    const minScores = dataList.value.map((item) => item.minScore || 0);
+    const avgScores = dataList.value.map((item) => item.avgScore || 0);
+    const stdevs = dataList.value.map((item) => item.stdev || 0);
+
+    chartOptions.value = {
+      toolbox: {
+        feature: {
+          saveAsImage: { show: true },
+        },
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'cross',
+          crossStyle: {
+            color: '#999',
+          },
+        },
+        formatter(params: any) {
+          let result = `${params[0].axisValue}<br/>`;
+          params.forEach((param: any) => {
+            const unit = param.seriesName.includes('率') ? '%' : '分';
+            result += `${param.marker}${param.seriesName}: ${param.value}${unit}<br/>`;
+          });
+          return result;
+        },
+      },
+      legend: {
+        data: ['满分', '平均分', '最高分', '最低分', '标准差'],
+        top: 30,
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '10%',
+        top: '15%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        data: names,
+        axisPointer: {
+          type: 'shadow',
+        },
+        axisLabel: {
+          rotate: 45,
+          interval: 0,
+        },
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: '分数',
+          position: 'left',
+        },
+        {
+          type: 'value',
+          name: '标准差',
+          position: 'right',
+        },
+      ],
+      series: [
+        {
+          name: '满分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: totalScores,
+          itemStyle: {
+            color: '#5470c6',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '最高分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: maxScores,
+          itemStyle: {
+            color: '#91cc75',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '最低分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: minScores,
+          itemStyle: {
+            color: '#fac858',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '平均分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: avgScores,
+          itemStyle: {
+            color: '#5ab1ef',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '标准差',
+          type: 'line',
+          yAxisIndex: 1,
+          data: stdevs,
+          lineStyle: {
+            color: '#ee6666',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#ee6666',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+      ],
+    };
+  }
+  const chartModalRef = ref<typeof ChartModal>(null);
+  function onChart() {
+    chartModalRef.value?.open();
+  }
 </script>

+ 158 - 3
src/views/analysis/ClassAnalysis.vue

@@ -6,7 +6,7 @@
           v-model="searchModel.subjectCode"
           :clearable="false"
           default-first
-          @default-selected="toPage(1)"
+          @default-selected="search"
         ></select-subject>
       </el-form-item>
       <el-form-item label="班级">
@@ -19,8 +19,9 @@
       </el-form-item>
       <el-form-item>
         <el-space wrap>
-          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button type="primary" @click="search">查询</el-button>
           <el-button @click="onExport">导出</el-button>
+          <el-button @click="onChart">查看统计图</el-button>
         </el-space>
       </el-form-item>
     </el-form>
@@ -66,14 +67,21 @@
       @current-change="toPage"
     />
   </div>
+  <ChartModal
+    ref="chartModalRef"
+    title="班级分析"
+    :chart-options="chartOptions"
+  />
 </template>
 
 <script setup lang="ts">
-  import { reactive } from 'vue';
+  import { reactive, ref } from 'vue';
   import { getClassAnalysisList } from '@/api/analysis';
   import { ClassAnalysisItem, ClassAnalysisFilter } from '@/api/types/analysis';
   import useTable from '@/hooks/table';
   import { downloadExport } from '@/utils/download-export';
+  import ChartModal from '@/components/chart/ChartModal.vue';
+  import type { EChartsOption } from 'echarts';
 
   defineOptions({
     name: 'ClassAnalysis',
@@ -87,7 +95,154 @@
   const { dataList, pagination, loading, toPage, pageSizeChange } =
     useTable<ClassAnalysisItem>(getClassAnalysisList, searchModel, false);
 
+  async function search() {
+    await toPage(1);
+    updateChartOption();
+  }
+
   async function onExport() {
     await downloadExport('exportClassAnalysisList', searchModel);
   }
+
+  // 图表
+  const chartOptions = ref<EChartsOption>();
+  function updateChartOption() {
+    const names = dataList.value.map((item) => item.className);
+    const avgScores = dataList.value.map((item) => item.avgScore || 0);
+    const maxScores = dataList.value.map((item) => item.maxScore || 0);
+    const minScores = dataList.value.map((item) => item.minScore || 0);
+    const passRates = dataList.value.map((item) => (item.passRate || 0) * 100);
+    const excellentRates = dataList.value.map(
+      (item) => (item.excellentRate || 0) * 100
+    );
+
+    chartOptions.value = {
+      toolbox: {
+        feature: {
+          saveAsImage: { show: true },
+        },
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'cross',
+          crossStyle: {
+            color: '#999',
+          },
+        },
+        formatter(params: any) {
+          let result = `${params[0].axisValue}<br/>`;
+          params.forEach((param: any) => {
+            const unit = param.seriesName.includes('率') ? '%' : '分';
+            result += `${param.marker}${param.seriesName}: ${param.value}${unit}<br/>`;
+          });
+          return result;
+        },
+      },
+      legend: {
+        data: ['平均分', '最高分', '最低分', '及格率', '优秀率'],
+        top: 30,
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '10%',
+        top: '15%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        data: names,
+        axisPointer: {
+          type: 'shadow',
+        },
+        axisLabel: {
+          rotate: 45,
+          interval: 0,
+        },
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: '分数',
+          position: 'left',
+        },
+        {
+          type: 'value',
+          name: '百分比',
+          position: 'right',
+          axisLabel: {
+            formatter: '{value} %',
+          },
+          max: 100,
+        },
+      ],
+      series: [
+        {
+          name: '平均分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: avgScores,
+          itemStyle: {
+            color: '#5470c6',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '最高分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: maxScores,
+          itemStyle: {
+            color: '#91cc75',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '最低分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: minScores,
+          itemStyle: {
+            color: '#fac858',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '及格率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: passRates,
+          lineStyle: {
+            color: '#ee6666',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#ee6666',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+        {
+          name: '优秀率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: excellentRates,
+          lineStyle: {
+            color: '#73c0de',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#73c0de',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+      ],
+    };
+  }
+  const chartModalRef = ref<typeof ChartModal>(null);
+  function onChart() {
+    chartModalRef.value?.open();
+  }
 </script>

+ 159 - 3
src/views/analysis/CollegeAnalysis.vue

@@ -6,7 +6,7 @@
           v-model="searchModel.subjectCode"
           :clearable="false"
           default-first
-          @default-selected="toPage(1)"
+          @default-selected="search"
         ></select-subject>
       </el-form-item>
       <el-form-item label="学生院系">
@@ -19,8 +19,9 @@
       </el-form-item>
       <el-form-item>
         <el-space wrap>
-          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button type="primary" @click="search">查询</el-button>
           <el-button @click="onExport">导出</el-button>
+          <el-button @click="onChart">查看统计图</el-button>
         </el-space>
       </el-form-item>
     </el-form>
@@ -66,10 +67,16 @@
       @current-change="toPage"
     />
   </div>
+
+  <ChartModal
+    ref="chartModalRef"
+    title="学院分析"
+    :chart-options="chartOptions"
+  />
 </template>
 
 <script setup lang="ts">
-  import { reactive } from 'vue';
+  import { reactive, ref } from 'vue';
   import { getCollegeAnalysisList } from '@/api/analysis';
   import {
     CollegeAnalysisItem,
@@ -77,6 +84,8 @@
   } from '@/api/types/analysis';
   import useTable from '@/hooks/table';
   import { downloadExport } from '@/utils/download-export';
+  import ChartModal from '@/components/chart/ChartModal.vue';
+  import type { EChartsOption } from 'echarts';
 
   defineOptions({
     name: 'CollegeAnalysis',
@@ -90,7 +99,154 @@
   const { dataList, pagination, loading, toPage, pageSizeChange } =
     useTable<CollegeAnalysisItem>(getCollegeAnalysisList, searchModel, false);
 
+  async function search() {
+    await toPage(1);
+    updateChartOption();
+  }
+
   async function onExport() {
     await downloadExport('exportCollegeAnalysisList', searchModel);
   }
+
+  // 图表
+  const chartOptions = ref<EChartsOption>();
+  function updateChartOption() {
+    const collegeNames = dataList.value.map((item) => item.collegeName);
+    const avgScores = dataList.value.map((item) => item.avgScore || 0);
+    const maxScores = dataList.value.map((item) => item.maxScore || 0);
+    const minScores = dataList.value.map((item) => item.minScore || 0);
+    const passRates = dataList.value.map((item) => (item.passRate || 0) * 100);
+    const excellentRates = dataList.value.map(
+      (item) => (item.excellentRate || 0) * 100
+    );
+
+    chartOptions.value = {
+      toolbox: {
+        feature: {
+          saveAsImage: { show: true },
+        },
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'cross',
+          crossStyle: {
+            color: '#999',
+          },
+        },
+        formatter(params: any) {
+          let result = `${params[0].axisValue}<br/>`;
+          params.forEach((param: any) => {
+            const unit = param.seriesName.includes('率') ? '%' : '分';
+            result += `${param.marker}${param.seriesName}: ${param.value}${unit}<br/>`;
+          });
+          return result;
+        },
+      },
+      legend: {
+        data: ['平均分', '最高分', '最低分', '及格率', '优秀率'],
+        top: 30,
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '10%',
+        top: '15%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        data: collegeNames,
+        axisPointer: {
+          type: 'shadow',
+        },
+        axisLabel: {
+          rotate: 45,
+          interval: 0,
+        },
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: '分数',
+          position: 'left',
+        },
+        {
+          type: 'value',
+          name: '百分比',
+          position: 'right',
+          axisLabel: {
+            formatter: '{value} %',
+          },
+          max: 100,
+        },
+      ],
+      series: [
+        {
+          name: '平均分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: avgScores,
+          itemStyle: {
+            color: '#5470c6',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '最高分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: maxScores,
+          itemStyle: {
+            color: '#91cc75',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '最低分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: minScores,
+          itemStyle: {
+            color: '#fac858',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '及格率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: passRates,
+          lineStyle: {
+            color: '#ee6666',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#ee6666',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+        {
+          name: '优秀率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: excellentRates,
+          lineStyle: {
+            color: '#73c0de',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#73c0de',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+      ],
+    };
+  }
+  const chartModalRef = ref<typeof ChartModal>(null);
+  function onChart() {
+    chartModalRef.value?.open();
+  }
 </script>

+ 139 - 3
src/views/analysis/ObjectiveQuestionAnalysis.vue

@@ -6,7 +6,7 @@
           v-model="searchModel.subjectCode"
           :clearable="false"
           default-first
-          @default-selected="toPage(1)"
+          @default-selected="search"
         ></select-subject>
       </el-form-item>
       <el-form-item label="试卷类型">
@@ -23,8 +23,9 @@
       </el-form-item>
       <el-form-item>
         <el-space wrap>
-          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button type="primary" @click="search">查询</el-button>
           <el-button @click="onExport">导出</el-button>
+          <el-button @click="onChart">查看统计图</el-button>
         </el-space>
       </el-form-item>
     </el-form>
@@ -73,10 +74,16 @@
       @current-change="toPage"
     />
   </div>
+
+  <ChartModal
+    ref="chartModalRef"
+    title="客观题分析"
+    :chart-options="chartOptions"
+  />
 </template>
 
 <script setup lang="ts">
-  import { reactive } from 'vue';
+  import { reactive, ref } from 'vue';
   import { getQuestionAnalysisList } from '@/api/analysis';
   import {
     QuestionAnalysisItem,
@@ -84,6 +91,8 @@
   } from '@/api/types/analysis';
   import useTable from '@/hooks/table';
   import { downloadExport } from '@/utils/download-export';
+  import ChartModal from '@/components/chart/ChartModal.vue';
+  import type { EChartsOption } from 'echarts';
 
   defineOptions({
     name: 'ObjectiveQuestionAnalysis',
@@ -98,7 +107,134 @@
   const { dataList, pagination, loading, toPage, pageSizeChange } =
     useTable<QuestionAnalysisItem>(getQuestionAnalysisList, searchModel, false);
 
+  async function search() {
+    await toPage(1);
+    updateChartOption();
+  }
+
   async function onExport() {
     await downloadExport('exportQuestionAnalysisList', searchModel);
   }
+
+  // 图表
+  const chartOptions = ref<EChartsOption>();
+  function updateChartOption() {
+    const names = dataList.value.map((item) => item.questionName);
+    const avgScores = dataList.value.map((item) => item.avgScore || 0);
+    const scoreRates = dataList.value.map(
+      (item) => (item.scoreRate || 0) * 100
+    );
+    const fullScoreRates = dataList.value.map(
+      (item) => (item.fullScoreRate || 0) * 100
+    );
+
+    chartOptions.value = {
+      toolbox: {
+        feature: {
+          saveAsImage: { show: true },
+        },
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'cross',
+          crossStyle: {
+            color: '#999',
+          },
+        },
+        formatter(params: any) {
+          let result = `${params[0].axisValue}<br/>`;
+          params.forEach((param: any) => {
+            const unit = param.seriesName.includes('率') ? '%' : '分';
+            result += `${param.marker}${param.seriesName}: ${param.value}${unit}<br/>`;
+          });
+          return result;
+        },
+      },
+      legend: {
+        data: ['单题平均分', '得分率', '满分率'],
+        top: 30,
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '10%',
+        top: '15%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        data: names,
+        axisPointer: {
+          type: 'shadow',
+        },
+        axisLabel: {
+          rotate: 45,
+          interval: 0,
+        },
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: '分数',
+          position: 'left',
+        },
+        {
+          type: 'value',
+          name: '百分比',
+          position: 'right',
+          axisLabel: {
+            formatter: '{value} %',
+          },
+          max: 100,
+        },
+      ],
+      series: [
+        {
+          name: '单题平均分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: avgScores,
+          itemStyle: {
+            color: '#5470c6',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '得分率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: scoreRates,
+          lineStyle: {
+            color: '#ee6666',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#ee6666',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+        {
+          name: '满分率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: fullScoreRates,
+          lineStyle: {
+            color: '#73c0de',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#73c0de',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+      ],
+    };
+  }
+  const chartModalRef = ref<typeof ChartModal>(null);
+  function onChart() {
+    chartModalRef.value?.open();
+  }
 </script>

+ 76 - 18
src/views/analysis/SegmentAnalysis.vue

@@ -6,7 +6,7 @@
           v-model="searchModel.subjectCode"
           :clearable="false"
           default-first
-          @default-selected="toPage(1)"
+          @default-selected="search"
         ></select-subject>
       </el-form-item>
       <el-form-item label="分数间隔类型">
@@ -27,8 +27,9 @@
       </el-form-item>
       <el-form-item>
         <el-space wrap>
-          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button type="primary" @click="search">查询</el-button>
           <el-button @click="onExport">导出</el-button>
+          <el-button @click="onChart">查看统计图</el-button>
         </el-space>
       </el-form-item>
     </el-form>
@@ -42,20 +43,16 @@
       stripe
     >
       <el-table-column type="index" label="序号" width="60" />
-      <el-table-column prop="range" label="分数段" min-width="120" />
-      <el-table-column prop="rangeCount" label="人数" min-width="100" />
-      <el-table-column label="频率" min-width="100">
+      <el-table-column prop="range" label="分数段" min-width="120">
         <template #default="scope">
-          {{ scope.row.rangeRate.toFixed(2) }}%
+          {{ scope.row.score }} &lt;=X&lt;
+          {{ scope.row.scope + scope.row.range }}
         </template>
       </el-table-column>
-      <el-table-column label="分布图" min-width="200">
+      <el-table-column prop="rangeCount" label="人数" min-width="100" />
+      <el-table-column label="频率" min-width="100">
         <template #default="scope">
-          <el-progress
-            :percentage="scope.row.rangeRate"
-            :color="getSegmentColor(scope.row.rangeRate)"
-            :show-text="false"
-          />
+          {{ scope.row.rangeRate.toFixed(2) }}%
         </template>
       </el-table-column>
     </el-table>
@@ -68,14 +65,22 @@
       @current-change="toPage"
     />
   </div>
+
+  <ChartModal
+    ref="chartModalRef"
+    title="课程成绩分段图"
+    :chart-options="chartOptions"
+  />
 </template>
 
 <script setup lang="ts">
-  import { reactive } from 'vue';
+  import { reactive, ref } from 'vue';
   import { getSegmentAnalysisList } from '@/api/analysis';
   import { ScoreSegmentItem, ScoreSegmentFilter } from '@/api/types/analysis';
   import useTable from '@/hooks/table';
   import { downloadExport } from '@/utils/download-export';
+  import ChartModal from '@/components/chart/ChartModal.vue';
+  import type { EChartsOption } from 'echarts';
 
   defineOptions({
     name: 'SegmentAnalysis',
@@ -90,14 +95,67 @@
   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';
+  async function search() {
+    await toPage(1);
+    updateChartOption();
   }
 
   async function onExport() {
     await downloadExport('exportSegmentAnalysisList', searchModel);
   }
+
+  // 图表
+  const chartOptions = ref<EChartsOption>({} as EChartsOption);
+  function updateChartOption() {
+    chartOptions.value = {
+      legend: {
+        data: ['人数', '频率'],
+      },
+      toolbox: {
+        feature: {
+          saveAsImage: { show: true },
+        },
+      },
+      xAxis: {
+        type: 'category',
+        data: dataList.value.map(
+          (item) => `${item.score} <=X< ${item.scope + item.range}`
+        ),
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: '人数',
+          position: 'left',
+        },
+        {
+          type: 'value',
+          name: '百分比',
+          position: 'right',
+          axisLabel: {
+            formatter: '{value} %',
+          },
+          max: 100,
+        },
+      ],
+      series: [
+        {
+          data: dataList.value.map((item) => item.rangeCount),
+          type: 'bar',
+          name: '人数',
+          yAxisIndex: 0,
+        },
+        {
+          data: dataList.value.map((item) => item.rangeRate),
+          type: 'line',
+          name: '频率',
+          yAxisIndex: 1,
+        },
+      ],
+    };
+  }
+  const chartModalRef = ref<typeof ChartModal>(null);
+  function onChart() {
+    chartModalRef.value?.open();
+  }
 </script>

+ 139 - 3
src/views/analysis/SubjectiveQuestionAnalysis.vue

@@ -6,13 +6,14 @@
           v-model="searchModel.subjectCode"
           :clearable="false"
           default-first
-          @default-selected="toPage(1)"
+          @default-selected="search"
         ></select-subject>
       </el-form-item>
       <el-form-item>
         <el-space wrap>
-          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button type="primary" @click="search">查询</el-button>
           <el-button @click="onExport">导出</el-button>
+          <el-button @click="onChart">查看统计图</el-button>
         </el-space>
       </el-form-item>
     </el-form>
@@ -61,10 +62,16 @@
       @current-change="toPage"
     />
   </div>
+
+  <ChartModal
+    ref="chartModalRef"
+    title="主观题分析"
+    :chart-options="chartOptions"
+  />
 </template>
 
 <script setup lang="ts">
-  import { reactive } from 'vue';
+  import { reactive, ref } from 'vue';
   import { getQuestionAnalysisList } from '@/api/analysis';
   import {
     QuestionAnalysisItem,
@@ -72,6 +79,8 @@
   } from '@/api/types/analysis';
   import useTable from '@/hooks/table';
   import { downloadExport } from '@/utils/download-export';
+  import ChartModal from '@/components/chart/ChartModal.vue';
+  import type { EChartsOption } from 'echarts';
 
   defineOptions({
     name: 'SubjectiveQuestionAnalysis',
@@ -85,7 +94,134 @@
   const { dataList, pagination, loading, toPage, pageSizeChange } =
     useTable<QuestionAnalysisItem>(getQuestionAnalysisList, searchModel, false);
 
+  async function search() {
+    await toPage(1);
+    updateChartOption();
+  }
+
   async function onExport() {
     await downloadExport('exportQuestionAnalysisList', searchModel);
   }
+
+  // 图表
+  const chartOptions = ref<EChartsOption>();
+  function updateChartOption() {
+    const names = dataList.value.map((item) => item.questionName);
+    const avgScores = dataList.value.map((item) => item.avgScore || 0);
+    const scoreRates = dataList.value.map(
+      (item) => (item.scoreRate || 0) * 100
+    );
+    const fullScoreRates = dataList.value.map(
+      (item) => (item.fullScoreRate || 0) * 100
+    );
+
+    chartOptions.value = {
+      toolbox: {
+        feature: {
+          saveAsImage: { show: true },
+        },
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'cross',
+          crossStyle: {
+            color: '#999',
+          },
+        },
+        formatter(params: any) {
+          let result = `${params[0].axisValue}<br/>`;
+          params.forEach((param: any) => {
+            const unit = param.seriesName.includes('率') ? '%' : '分';
+            result += `${param.marker}${param.seriesName}: ${param.value}${unit}<br/>`;
+          });
+          return result;
+        },
+      },
+      legend: {
+        data: ['单题平均分', '得分率', '满分率'],
+        top: 30,
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '10%',
+        top: '15%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        data: names,
+        axisPointer: {
+          type: 'shadow',
+        },
+        axisLabel: {
+          rotate: 45,
+          interval: 0,
+        },
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: '分数',
+          position: 'left',
+        },
+        {
+          type: 'value',
+          name: '百分比',
+          position: 'right',
+          axisLabel: {
+            formatter: '{value} %',
+          },
+          max: 100,
+        },
+      ],
+      series: [
+        {
+          name: '单题平均分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: avgScores,
+          itemStyle: {
+            color: '#5470c6',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '得分率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: scoreRates,
+          lineStyle: {
+            color: '#ee6666',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#ee6666',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+        {
+          name: '满分率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: fullScoreRates,
+          lineStyle: {
+            color: '#73c0de',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#73c0de',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+      ],
+    };
+  }
+  const chartModalRef = ref<typeof ChartModal>(null);
+  function onChart() {
+    chartModalRef.value?.open();
+  }
 </script>

+ 177 - 3
src/views/analysis/TeacherAnalysis.vue

@@ -6,7 +6,7 @@
           v-model="searchModel.subjectCode"
           :clearable="false"
           default-first
-          @default-selected="toPage(1)"
+          @default-selected="search"
         ></select-subject>
       </el-form-item>
       <el-form-item label="任课老师">
@@ -19,8 +19,9 @@
       </el-form-item>
       <el-form-item>
         <el-space wrap>
-          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button type="primary" @click="search">查询</el-button>
           <el-button @click="onExport">导出</el-button>
+          <el-button @click="onChart">查看统计图</el-button>
         </el-space>
       </el-form-item>
     </el-form>
@@ -81,10 +82,16 @@
       @current-change="toPage"
     />
   </div>
+
+  <ChartModal
+    ref="chartModalRef"
+    title="任课老师分析"
+    :chart-options="chartOptions"
+  />
 </template>
 
 <script setup lang="ts">
-  import { reactive } from 'vue';
+  import { reactive, ref } from 'vue';
   import { useRouter } from 'vue-router';
   import { getTeacherAnalysisList } from '@/api/analysis';
   import {
@@ -94,6 +101,8 @@
   import useTable from '@/hooks/table';
   import ls from '@/utils/storage';
   import { downloadExport } from '@/utils/download-export';
+  import ChartModal from '@/components/chart/ChartModal.vue';
+  import type { EChartsOption } from 'echarts';
 
   defineOptions({
     name: 'TeacherAnalysis',
@@ -108,6 +117,11 @@
   const { dataList, pagination, loading, toPage, pageSizeChange } =
     useTable<TeacherAnalysisItem>(getTeacherAnalysisList, searchModel, false);
 
+  async function search() {
+    await toPage(1);
+    updateChartOption();
+  }
+
   async function onExport() {
     await downloadExport('exportTeacherAnalysisList', searchModel);
   }
@@ -122,4 +136,164 @@
       name: 'TeacherClassAnalysis',
     });
   }
+
+  // 图表
+  const chartOptions = ref<EChartsOption>();
+  function updateChartOption() {
+    const collegeNames = dataList.value.map((item) => item.collegeName);
+    const avgScores = dataList.value.map((item) => item.avgScore || 0);
+    const maxScores = dataList.value.map((item) => item.maxScore || 0);
+    const minScores = dataList.value.map((item) => item.minScore || 0);
+    const passRates = dataList.value.map((item) => (item.passRate || 0) * 100);
+    const excellentRates = dataList.value.map(
+      (item) => (item.excellentRate || 0) * 100
+    );
+    const relativeAvgScores = dataList.value.map(
+      (item) => item.relativeAvgScore || 0
+    );
+
+    chartOptions.value = {
+      toolbox: {
+        feature: {
+          saveAsImage: { show: true },
+        },
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'cross',
+          crossStyle: {
+            color: '#999',
+          },
+        },
+        formatter(params: any) {
+          let result = `${params[0].axisValue}<br/>`;
+          params.forEach((param: any) => {
+            const unit = param.seriesName.includes('率') ? '%' : '分';
+            result += `${param.marker}${param.seriesName}: ${param.value}${unit}<br/>`;
+          });
+          return result;
+        },
+      },
+      legend: {
+        data: ['平均分', '最高分', '最低分', '相对平均分', '及格率', '优秀率'],
+        top: 30,
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '10%',
+        top: '15%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        data: collegeNames,
+        axisPointer: {
+          type: 'shadow',
+        },
+        axisLabel: {
+          rotate: 45,
+          interval: 0,
+        },
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: '分数',
+          position: 'left',
+        },
+        {
+          type: 'value',
+          name: '百分比',
+          position: 'right',
+          axisLabel: {
+            formatter: '{value} %',
+          },
+          max: 100,
+        },
+      ],
+      series: [
+        {
+          name: '平均分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: avgScores,
+          itemStyle: {
+            color: '#5470c6',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '最高分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: maxScores,
+          itemStyle: {
+            color: '#91cc75',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '最低分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: minScores,
+          itemStyle: {
+            color: '#fac858',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '相对平均分',
+          type: 'line',
+          yAxisIndex: 1,
+          data: relativeAvgScores,
+          lineStyle: {
+            color: '#ed8884',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#ed8884',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+        {
+          name: '及格率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: passRates,
+          lineStyle: {
+            color: '#ee6666',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#ee6666',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+        {
+          name: '优秀率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: excellentRates,
+          lineStyle: {
+            color: '#73c0de',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#73c0de',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+      ],
+    };
+  }
+  const chartModalRef = ref<typeof ChartModal>(null);
+  function onChart() {
+    chartModalRef.value?.open();
+  }
 </script>

+ 183 - 3
src/views/analysis/TeacherClassAnalysis.vue

@@ -9,9 +9,10 @@
       </el-form-item>
       <el-form-item>
         <el-space wrap>
-          <el-button type="primary" @click="toPage(1)">查询</el-button>
+          <el-button type="primary" @click="search">查询</el-button>
           <el-button @click="onExport">导出</el-button>
-          <el-button @click="onExport">查看统计图</el-button>
+          <el-button @click="onChart">查看统计图</el-button>
+          <el-button @click="onBack">返回</el-button>
         </el-space>
       </el-form-item>
     </el-form>
@@ -65,10 +66,17 @@
       @current-change="toPage"
     />
   </div>
+
+  <ChartModal
+    ref="chartModalRef"
+    title="任课老师分析"
+    :chart-options="chartOptions"
+  />
 </template>
 
 <script setup lang="ts">
-  import { reactive, onUnmounted } from 'vue';
+  import { reactive, ref, onUnmounted } from 'vue';
+  import { useRouter } from 'vue-router';
   import { getTeacherClassAnalysisList } from '@/api/analysis';
   import {
     TeacherClassAnalysisItem,
@@ -77,6 +85,8 @@
   import useTable from '@/hooks/table';
   import ls from '@/utils/storage';
   import { downloadExport } from '@/utils/download-export';
+  import ChartModal from '@/components/chart/ChartModal.vue';
+  import type { EChartsOption } from 'echarts';
 
   defineOptions({
     name: 'TeacherClassAnalysis',
@@ -94,6 +104,7 @@
       teacherName: '',
     } as TeacherInfo)
   );
+  const router = useRouter();
 
   const searchModel = reactive<TeacherClassAnalysisFilter>({
     subjectCode: teacherInfo.subjectCode,
@@ -107,11 +118,180 @@
       false
     );
 
+  async function search() {
+    await toPage(1);
+    updateChartOption();
+  }
+
   async function onExport() {
     await downloadExport('exportClassAnalysisList', searchModel);
   }
 
+  function onBack() {
+    router.push({ name: 'TeacherAnalysis' });
+  }
+
   onUnmounted(() => {
     ls.remove('teacherClassAnalysis');
   });
+
+  // 图表
+  const chartOptions = ref<EChartsOption>();
+  function updateChartOption() {
+    const collegeNames = dataList.value.map((item) => item.collegeName);
+    const avgScores = dataList.value.map((item) => item.avgScore || 0);
+    const maxScores = dataList.value.map((item) => item.maxScore || 0);
+    const minScores = dataList.value.map((item) => item.minScore || 0);
+    const passRates = dataList.value.map((item) => (item.passRate || 0) * 100);
+    const excellentRates = dataList.value.map(
+      (item) => (item.excellentRate || 0) * 100
+    );
+    const relativeAvgScores = dataList.value.map(
+      (item) => item.relativeAvgScore || 0
+    );
+
+    chartOptions.value = {
+      toolbox: {
+        feature: {
+          saveAsImage: { show: true },
+        },
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'cross',
+          crossStyle: {
+            color: '#999',
+          },
+        },
+        formatter(params: any) {
+          let result = `${params[0].axisValue}<br/>`;
+          params.forEach((param: any) => {
+            const unit = param.seriesName.includes('率') ? '%' : '分';
+            result += `${param.marker}${param.seriesName}: ${param.value}${unit}<br/>`;
+          });
+          return result;
+        },
+      },
+      legend: {
+        data: ['平均分', '最高分', '最低分', '相对平均分', '及格率', '优秀率'],
+        top: 30,
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '10%',
+        top: '15%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        data: collegeNames,
+        axisPointer: {
+          type: 'shadow',
+        },
+        axisLabel: {
+          rotate: 45,
+          interval: 0,
+        },
+      },
+      yAxis: [
+        {
+          type: 'value',
+          name: '分数',
+          position: 'left',
+        },
+        {
+          type: 'value',
+          name: '百分比',
+          position: 'right',
+          axisLabel: {
+            formatter: '{value} %',
+          },
+          max: 100,
+        },
+      ],
+      series: [
+        {
+          name: '平均分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: avgScores,
+          itemStyle: {
+            color: '#5470c6',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '最高分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: maxScores,
+          itemStyle: {
+            color: '#91cc75',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '最低分',
+          type: 'bar',
+          yAxisIndex: 0,
+          data: minScores,
+          itemStyle: {
+            color: '#fac858',
+          },
+          barWidth: '15%',
+        },
+        {
+          name: '相对平均分',
+          type: 'line',
+          yAxisIndex: 1,
+          data: relativeAvgScores,
+          lineStyle: {
+            color: '#ed8884',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#ed8884',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+        {
+          name: '及格率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: passRates,
+          lineStyle: {
+            color: '#ee6666',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#ee6666',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+        {
+          name: '优秀率',
+          type: 'line',
+          yAxisIndex: 1,
+          data: excellentRates,
+          lineStyle: {
+            color: '#73c0de',
+            width: 3,
+          },
+          itemStyle: {
+            color: '#73c0de',
+          },
+          symbol: 'circle',
+          symbolSize: 6,
+        },
+      ],
+    };
+  }
+  const chartModalRef = ref<typeof ChartModal>(null);
+  function onChart() {
+    chartModalRef.value?.open();
+  }
 </script>

+ 6 - 6
src/views/exam/ExamEdit.vue

@@ -363,10 +363,9 @@
       id: undefined,
       name: '',
       type: 'SCAN_IMAGE',
-      examTime: '',
       startTime: '',
       endTime: '',
-      status: '',
+      status: 'START',
       markMode: 'TRACK',
       forceSpecialTag: false,
       forbiddenInfo: false,
@@ -385,6 +384,7 @@
       enableSplit: false,
       description: '',
       // 高级配置默认值
+      markScrollBottom: false,
       minMarkDuration: undefined,
       reMarkLimitCount: undefined,
       trackCountPolicy: null,
@@ -398,12 +398,12 @@
 
   // 高级配置数据
   const advancedConfig = computed<ExamAdvancedConfig>(() => ({
-    markScrollBottom: formModel.inspectScrollBottom || false,
+    markScrollBottom: formModel.markScrollBottom,
     minMarkDuration: formModel.minMarkDuration,
     reMarkLimitCount: formModel.reMarkLimitCount,
     trackCountPolicy: formModel.trackCountPolicy,
-    barcodeAiCheck: formModel.barcodeAiCheck || false,
-    answerAiCheck: formModel.answerAiCheck || false,
+    barcodeAiCheck: formModel.barcodeAiCheck,
+    answerAiCheck: formModel.answerAiCheck,
   }));
 
   // 表单验证规则
@@ -473,7 +473,7 @@
 
   // 返回
   function goBack() {
-    router.back();
+    router.push({ name: 'ExamManage' });
   }
 
   onMounted(() => {