liurongli 3 dní pred
rodič
commit
1e63b9d93f

+ 16 - 0
src/api/analysis.ts

@@ -59,4 +59,20 @@ export const classContrastSubjectTable = (data: any): Promise<ApiResponse> => {
     method: "post",
     data,
   });
+};
+// ==========================================小题分析============================================
+export const questionAnalysis = (data: any): Promise<ApiResponse> => {
+  return request({
+    url: "/api/v1/ai_analysis/questionAnalysis",
+    method: "post",
+    data,
+  });
+};
+// 通过答案或者分数查询某题作答情况
+export const queryAnswerListByAnswerAndScore = (data: any): Promise<ApiResponse> => {
+  return request({
+    url: "/api/v1/ai_analysis/queryAnswerListByAnswerAndScore",
+    method: "post",
+    data,
+  });
 };

BIN
src/assets/icon/card_view.webp


+ 7 - 1
src/components/ReportModule.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="report_module">
-        <div class="module_title" v-if="tableOrChart!='qita'">
+        <div class="module_title" v-if="showHeader">
             <div class="title_left">
                 <template v-if="showTitle && titleList.length">
                     <template v-for="(item, index) in titleList">
@@ -52,6 +52,7 @@
 import { onMounted, reactive, nextTick, ref } from 'vue'
 
 interface TitleListType {
+    showHeader?: boolean
     titleList?: string[]
     showTitle?: boolean
     showPrintBtn?: boolean
@@ -66,6 +67,7 @@ interface TitleListType {
 }
 
 const props = withDefaults(defineProps<TitleListType>(), {
+    showHeader: true,
     titleList: () => [],//标题
     showTitle: true,//是否显示标题
     showPrintBtn: true,//是否显示打印按钮
@@ -218,6 +220,10 @@ onMounted(() => {
         padding: 0 20px 14px;
         box-sizing: border-box;
         border-collapse: collapse;
+        :deep(.el-table){
+            border-left: 0px;
+            border-right: 0px;
+        }
 
         :deep(.table_row_blue) {
             color: #2e64fa;

+ 405 - 0
src/components/echarts/barChart_answer.vue

@@ -0,0 +1,405 @@
+<template>
+  <div ref="barEchartRef" class="chart_width" :style="{ height: `${height}px` }" />
+</template>
+
+<script setup lang="ts">
+import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
+import throttle from "lodash/throttle";
+import type { ECharts, EChartsOption, BarSeriesOption } from "echarts";
+import * as echarts from "echarts";
+import { getScorePerformanceAnalysis } from "@/utils/common";
+
+// ====================== 类型定义 ======================
+type DataArr = (string | number)[];
+
+// ====================== Props ======================
+interface Props {
+  datax?: DataArr;
+  datay?: DataArr;
+  answerScore?: DataArr;
+  color?: string;
+  height?: number;
+  unit?: string;
+  showNuitY?: boolean;
+  showSplitArea?: boolean;
+  showTooltip?: boolean;
+  tooltipData?: Array<{ list: Array<{ name: string; value: string | number }> }>;
+  typeName?: string;
+  average?: string | number;
+  isShowMarkLine?: boolean;
+  showMarkPoint?: boolean;
+  isClick?: boolean;
+  answerValue?: string;
+  unitX?: string;
+  fullMark?: string | number;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  datax: () => [],
+  datay: () => [],
+  answerScore: () => [],
+  color: "#5470C6",
+  height: 380,
+  unit: "%",
+  showNuitY: true,
+  showSplitArea: false,
+  showTooltip: true,
+  tooltipData: () => [],
+  typeName: "",
+  average: "0",
+  isShowMarkLine: true,
+  showMarkPoint: false,
+  isClick: false,
+  answerValue: "",
+  unitX: "",
+  fullMark: "",
+});
+
+// ====================== Emits ======================
+interface Emits {
+  (e: "HandleChartClick", index: number, xName: string): void;
+}
+const emit = defineEmits<Emits>();
+
+// ====================== 响应式状态 ======================
+const barEchartRef = ref<HTMLDivElement | null>(null);
+let echartInstance: ECharts | null = null;
+const currentIndex = ref<number>(0);
+
+// ====================== 工具方法 ======================
+/** 销毁图表实例 */
+function disposeEchart() {
+  if (echartInstance) {
+    echartInstance.dispose();
+    echartInstance = null;
+  }
+}
+
+/** 窗口自适应节流函数 */
+const handleResize = throttle(async () => {
+  await nextTick();
+  LoadEchart();
+}, 500);
+
+/** 加载渲染图表 */
+async function LoadEchart() {
+  if (!barEchartRef.value) return;
+  disposeEchart();
+
+  echartInstance = echarts.init(barEchartRef.value, null, { devicePixelRatio: 2 });
+
+  const datax = props.datax;
+  const datay = props.datay;
+  const answerScore = props.answerScore;
+  const rightAnswerValue = props.answerValue || "";
+  const fullMark = props.fullMark;
+  const typeName = props.typeName || "";
+  const unit = props.unit;
+  const yAxisUnit = props.showNuitY ? unit : "";
+  const totalWidth = barEchartRef.value.clientWidth;
+
+  // 颜色数组
+  const colors: string[] = [];
+  for (let i = 0; i < datax.length; i++) {
+    if (props.color) {
+      colors.push(props.color);
+    } else {
+      const colorArr = getScorePerformanceAnalysis();
+      colors.push(colorArr[i] ?? "#5470C6");
+    }
+  }
+
+  // 计算Y轴极值
+  const numDatay = datay.map((v) => Number(v));
+  const maxValue = Math.max(...numDatay);
+  const minValue = Math.min(...numDatay);
+  const average = Number(props.average) || 0;
+  const nearestValue = Math.ceil((maxValue + maxValue * 0.2) / 10) * 10;
+
+  // dataZoom 计算
+  const barMinWidth = 30;
+  const dataZoomNum = Math.floor((totalWidth - 47) / barMinWidth);
+  const dataZoomEnd = datax.length ? Math.floor((100 / datax.length) * dataZoomNum) : 0;
+  let isShowDataZoom = ((totalWidth - 97) / datax.length) < barMinWidth;
+  let singleSeriesWidth = ((totalWidth - 97) / datax.length).toFixed(2);
+  if (isShowDataZoom) singleSeriesWidth = String(barMinWidth);
+
+  const dataZoom = {
+    start: 0,
+    end: dataZoomEnd,
+    type: "slider",
+    show: true,
+    borderColor: "transparent",
+    borderCap: "round",
+    xAxisIndex: [0],
+    height: 8,
+    left: 20,
+    right: 20,
+    bottom: 0,
+    fillerColor: "transparent",
+    zoomLock: true,
+    handleSize: "0",
+    handleStyle: {
+      color: "#b8b8b8",
+      borderWidth: 2,
+    },
+    backgroundColor: "transparent",
+    showDataShadow: false,
+    showDetail: false,
+    filterMode: "filter",
+  };
+
+  const splitArea = props.showSplitArea
+    ? { show: true, areaStyle: { color: ["#fafafa", "#ffffff"] } }
+    : {};
+
+  // 构造系列数据
+  const seriesData = datay.map((value) => {
+    const numVal = Number(value);
+    return {
+      value: numVal,
+      label: {
+        show: props.showMarkPoint ? (numVal === 0 || numVal === maxValue || numVal === minValue) : true,
+        position: "top" as const,
+        fontSize: 14,
+        formatter: `{c}${unit}`,
+        color: "#666",
+      },
+    };
+  });
+
+  // markPoint 数据
+  const markPointData = datay
+    .map((value, index) => {
+      const valNum = Number(value);
+      if (valNum === maxValue || valNum === minValue) {
+        return {
+          name: valNum === maxValue ? "最大值" : "最小值",
+          coord: [datax[index], valNum],
+          symbolSize: 65,
+          label: {
+            show: true,
+            position: "inside" as const,
+            color: "#fff",
+            formatter: `${valNum}${unit}`,
+          },
+          itemStyle: { color: props.color },
+        };
+      }
+      return null;
+    })
+    .filter((item): item is NonNullable<typeof item> => item !== null);
+
+  const series: BarSeriesOption[] = [
+    {
+      name: "阅卷进度",
+      type: "bar",
+      barMaxWidth: 50,
+      barMinWidth: 20,
+      itemStyle: {
+        color: (params) => {
+          if (String(params.name) === rightAnswerValue || String(params.name) === String(fullMark)) {
+            return "#3BA272";
+          }
+          return colors[params.dataIndex] ?? "#5470C6";
+        },
+      },
+      data: seriesData,
+      markLine: {
+        symbol: average === 0 ? ["none", "none"] : ["circle", "arrow"],
+        symbolSize: [8, 8],
+        symbolOffset: [[0, 0], [0, 0]],
+        label: {
+          color: "#F56C6C",
+          fontSize: 15,
+          formatter: `{c}${unit}`,
+          position: "end",
+        },
+        data: average !== 0 ? [{ type: "value", name: "", yAxis: average }] : [],
+        lineStyle: { color: "#F56C6C" },
+      },
+      markPoint: props.showMarkPoint
+        ? {
+            data: markPointData,
+            label: { show: true, fontSize: 10, fontWeight: "bold" },
+          }
+        : undefined,
+    },
+  ];
+
+  const option: EChartsOption = {
+    tooltip: {
+      show: props.showTooltip,
+      trigger: props.showTooltip ? "axis" : "item",
+      triggerOn: "mousemove",
+      renderMode: "html",
+      confine: true,
+      extraCssText:
+        "border-radius: 4px;padding:5px 0px 5px 5px;white-space:normal;word-wrap:break-word;max-width: 400px;",
+      borderColor: "#fff",
+      formatter: (params: any[]) => {
+        const p = params[0] ?? params;
+        let tooltip = `<div class='tooltip_content'>`;
+        const title = p?.name ?? "";
+        const value = p?.value ?? "";
+        tooltip += `<div class='tooltip_title'>${title}</div>`;
+        tooltip += `<div class='tooltip_student'>${typeName}:${value}${unit}</div>`;
+        if (props.tooltipData.length > 0 && p.dataIndex != null) {
+          const list = props.tooltipData[p.dataIndex]?.list ?? [];
+          list.forEach((item) => {
+            tooltip += `<div class='tooltip_student'>${item.name}:${item.value}</div>`;
+          });
+        }
+        tooltip += `</div>`;
+        return tooltip;
+      },
+    },
+    grid: {
+      left: 20,
+      right: 27,
+      top: 20,
+      bottom: 0,
+      containLabel: true,
+    },
+    dataZoom: isShowDataZoom ? [dataZoom] : undefined,
+    xAxis: [
+      {
+        type: "category",
+        data: datax,
+        axisPointer: { type: "shadow" },
+        axisLabel: {
+          interval: 0,
+          rotate: Number(singleSeriesWidth) < 60 ? 45 : 0,
+          color: "#666666",
+          width: 80,
+          overflow: "truncate",
+          ellipsis: "...",
+          fontSize: 14,
+          formatter: (value: string) => {
+            const w = value.length * 16;
+            const sw = Number(singleSeriesWidth);
+            if (w > sw) {
+              if (sw < 60) return value;
+              const maxLen = Math.floor(sw / 16);
+              return value.slice(0, maxLen) + "...";
+            }
+            return value;
+          },
+        },
+      },
+    ],
+    yAxis: {
+      type: "value",
+      axisLabel: {
+        color: "#666666",
+        fontSize: 14,
+        formatter: `{value}${yAxisUnit}`,
+      },
+      splitArea,
+    },
+    series,
+  };
+
+  echartInstance.setOption(option);
+
+  // 点击事件
+  if (props.isClick) {
+    const gridRect = echartInstance.getModel().getComponent("grid").coordinateSystem.getRect();
+    const xAxisDataLength = datax.length;
+    let calcSingleLabelWidth = xAxisDataLength ? gridRect.width / xAxisDataLength : 0;
+    if (isShowDataZoom) calcSingleLabelWidth = barMinWidth;
+
+    echartInstance.off("click");
+    echartInstance.on("click", (params: any) => {
+      if (params.seriesType === "bar") {
+        const idx = params.dataIndex;
+        currentIndex.value = idx;
+        const xName = String(params.name);
+        const pixelPosition = echartInstance!.convertToPixel({ xAxisIndex: 0 }, xName);
+        echartInstance!.setOption({
+          graphic: {
+            id: "highlight-box",
+            type: "rect",
+            shape: {
+              x: pixelPosition - calcSingleLabelWidth / 2,
+              y: gridRect.y,
+              width: calcSingleLabelWidth,
+              height: gridRect.height,
+            },
+            style: { fill: params.color + "30" },
+          },
+        });
+        emit("HandleChartClick", idx, xName);
+      }
+    });
+
+    // 默认高亮第一个
+    if (datax.length > 0) {
+      const defaultPixelPosition = echartInstance.convertToPixel({ xAxisIndex: 0 }, datax[0]);
+      echartInstance.setOption({
+        graphic: {
+          id: "highlight-box",
+          type: "rect",
+          shape: {
+            x: defaultPixelPosition - calcSingleLabelWidth / 2,
+            y: gridRect.y,
+            width: calcSingleLabelWidth,
+            height: gridRect.height,
+          },
+          style: { fill: "rgba(84,112,198,0.1)" },
+        },
+      });
+    }
+  }
+
+  // dataZoom 事件
+  echartInstance.off("datazoom");
+  echartInstance.on("datazoom", () => {
+    const gridRect = echartInstance!.getModel().getComponent("grid").coordinateSystem.getRect();
+    let calcSingleLabelWidth = props.datax.length ? gridRect.width / props.datax.length : 0;
+    if (isShowDataZoom) calcSingleLabelWidth = Number(singleSeriesWidth);
+
+    const idx = currentIndex.value;
+    if (props.datax.length > idx) {
+      const xName = props.datax[idx];
+      const pixelPosition = echartInstance!.convertToPixel({ xAxisIndex: 0 }, xName);
+      echartInstance!.setOption({
+        graphic: {
+          id: "highlight-box",
+          type: "rect",
+          shape: {
+            x: pixelPosition - calcSingleLabelWidth / 2,
+            y: gridRect.y,
+            width: calcSingleLabelWidth,
+            height: gridRect.height,
+          },
+          style: { fill: "rgba(84,112,198,0.1)" },
+        },
+      });
+    }
+  });
+
+  window.addEventListener("resize", handleResize);
+}
+
+// ====================== 监听 & 生命周期 ======================
+watch(
+  () => props.datay,
+  () => LoadEchart(),
+  { deep: true }
+);
+
+onMounted(() => LoadEchart());
+
+onBeforeUnmount(() => {
+  window.removeEventListener("resize", handleResize);
+  disposeEchart();
+});
+</script>
+
+<style lang="scss" scoped>
+.chart_width {
+  width: 100%;
+  height: 380px;
+}
+</style>

+ 489 - 0
src/components/echarts/barLineCharts.vue

@@ -0,0 +1,489 @@
+<template>
+  <div class="echart_content" style="position: relative">
+    <div class="is_show_all" v-if="showCheckBox">
+      <el-checkbox
+        v-model="checkAll"
+        :indeterminate="isIndeterminate"
+        @change="ChangeCheckAll"
+      >
+        显示全部
+      </el-checkbox>
+    </div>
+
+    <div class="title_right" v-if="selectBarNum === 1">
+      <div
+        :class="['right_item', { item_active: selectValue === item.value }]"
+        v-for="item in selectOptions"
+        :key="item.value"
+        @click="ChangeOrder(item.value)"
+      >
+        {{ item.label }}
+      </div>
+    </div>
+
+    <div ref="barChartsRef" class="chart_width" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
+import throttle from "lodash/throttle";
+// 修复:替换不存在的 LegendOption
+import type { ECharts, EChartsOption, LegendComponentOption } from "echarts";
+import * as echarts from "echarts";
+import { getScorePerformanceAnalysis } from "@/utils/common";
+
+// ==================== Types ====================
+type SelectOption = {
+  value: string;
+  label: string;
+};
+
+type ChartDataRow = (string | number)[];
+type ChartDatasetSource = ChartDataRow[];
+
+// 组件 Props
+interface Props {
+  showCheckBox?: boolean;
+  color?: string[];
+  data?: ChartDatasetSource;
+  title?: string;
+  legendList?: string[];
+  showBarLegendIndex?: number;
+  showSortSelectbox?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  showCheckBox: true,
+  color: () => [],
+  data: () => [],
+  title: "",
+  legendList: () => [],
+  showBarLegendIndex: 1,
+  showSortSelectbox: true,
+});
+
+// Emits
+interface Emits {
+  (e: "HandleChartClick", index: number, name: string): void;
+  (
+    e: "ChangeChartOrder",
+    val: string,
+    legendData: string[],
+    selectedBarIndex: number,
+  ): void;
+}
+const emit = defineEmits<Emits>();
+
+// ==================== Ref State ====================
+const barChartsRef = ref<HTMLDivElement | null>(null);
+let echartInstance: ECharts | null = null;
+
+const selectValue = ref<string>("1");
+const selectOptions = ref<SelectOption[]>([
+  { value: "1", label: "默认" },
+  { value: "2", label: "升序" },
+  { value: "3", label: "降序" },
+]);
+const selectBarNum = ref<number>(0);
+const checkAll = ref<boolean>(true);
+const isIndeterminate = ref<boolean>(false);
+const colors = ref<string[]>(getScorePerformanceAnalysis());
+const legendSelected = ref<Record<string, boolean>>({});
+const legenSelectList = ref<string[]>([]);
+const legenAllList = ref<string[]>([]);
+const dataZoomEnd = ref<number>(0);
+const dataZoomNum = ref<number>(5);
+
+// ==================== Methods ====================
+/** 销毁实例 + 移除监听 */
+function disposeEchart() {
+  if (echartInstance) {
+    echartInstance.dispose();
+    echartInstance = null;
+  }
+}
+
+/** X轴文字悬浮提示(全局div) */
+function LabelMouseOver() {
+  // 填入你原有DOM逻辑
+}
+function ShowXAxisTooltip() {
+  // 填入你原有DOM逻辑
+}
+
+/** 加载渲染图表 */
+async function LoadEchart() {
+  if (!barChartsRef.value) return;
+  disposeEchart();
+
+  echartInstance = echarts.init(barChartsRef.value, null, {
+    devicePixelRatio: 2,
+  });
+
+  // 修复:转换为 string[],消除TS2322类型报错
+  if (props.legendList.length > 0) {
+    legenSelectList.value = [...props.legendList];
+  } else {
+    legenSelectList.value =
+      props.data[0]?.slice(1).map((item) => String(item)) ?? [];
+  }
+  legenAllList.value =
+    props.data[0]?.slice(1).map((item) => String(item)) ?? [];
+
+  // dataset
+  const dataset = { source: props.data };
+  let constColorAll = getScorePerformanceAnalysis();
+  if (props.color.length > 0) constColorAll = props.color;
+
+  // 初始化图例选中状态
+  const legendMap: Record<string, boolean> = {};
+  const dims = dataset.source[0]?.slice(1) ?? [];
+  dims.forEach((dim) => {
+    legendMap[String(dim)] = false;
+  });
+  legenSelectList.value.forEach((item) => {
+    legendMap[item] = true;
+  });
+  legendSelected.value = legendMap;
+
+  checkAll.value = legenSelectList.value.length === legenAllList.value.length;
+  isIndeterminate.value =
+    legenSelectList.value.length > 0 &&
+    legenSelectList.value.length < legenAllList.value.length;
+
+  const numSeries = Object.values(legendSelected.value).filter(Boolean).length;
+  const totalWidth = barChartsRef.value.clientWidth;
+  const dataxCount = props.data.length;
+
+  dataZoomNum.value = Math.floor((totalWidth - 60) / (50 * numSeries));
+  const xAxisTotal = dataset.source.length - 1 || 1;
+  dataZoomEnd.value = Math.floor((100 / xAxisTotal) * dataZoomNum.value);
+
+  const dataZoom = {
+    start: 0,
+    end: dataZoomEnd.value,
+    type: "slider",
+    show: true,
+    borderColor: "transparent",
+    borderCap: "round",
+    xAxisIndex: [0],
+    height: 8,
+    left: 20,
+    right: 20,
+    bottom: 0,
+    fillerColor: "transparent",
+    zoomLock: true,
+    handleSize: "0",
+    handleStyle: {
+      color: "#b8b8b8",
+      borderWidth: 2,
+    },
+    backgroundColor: "transparent",
+    showDataShadow: false,
+    showDetail: false,
+    filterMode: "filter",
+  };
+
+  // series 构建
+  const series = dims.map((item, index) => {
+    const name = String(item);
+    if (index < props.showBarLegendIndex) {
+      return {
+        type: "bar",
+        name,
+        barGap: 0,
+        barCategoryGap: "30%",
+        barMinWidth: 8,
+        barMaxWidth: 50,
+        itemStyle: {
+          color: constColorAll[index] ?? "#ccc",
+        },
+        label: {
+          position: "top",
+          color: "#666",
+          fontSize: 12,
+          formatter: (params: any) => {
+            const val = params.value[params.seriesIndex + 1];
+            return `${val}%`;
+          },
+        },
+      };
+    } else {
+      return {
+        type: "line",
+        name,
+        label: { show: false },
+        symbol: "emptyCircle",
+        showSymbol: true,
+        smooth: false,
+        itemStyle: {
+          color: constColorAll[index] ?? "#ccc",
+          borderColor: constColorAll[index] ?? "#ccc",
+          borderWidth: 3,
+        },
+      };
+    }
+  });
+
+  const option: EChartsOption = {
+    dataset,
+    tooltip: {
+      axisPointer: { type: "line" },
+      trigger: "axis",
+      confine: true,
+      extraCssText:
+        "border-radius: 4px;padding:5px 0px 5px 5px;white-space:normal;word-wrap:break-word;max-width: 400px;",
+      enterable: true,
+      formatter: (params: any[]) => {
+        let tooltip = `<div class='tooltip_content'>`;
+        const title = params[0].name;
+        tooltip += `<div class='tooltip_title'>${title}</div>`;
+        params.forEach((item) => {
+          const className = item.seriesName;
+          const rate = item.data[item.seriesIndex + 1];
+          const iconClass =
+            item.seriesType === "bar"
+              ? "tooltip_rect_icon"
+              : "tooltip_line_icon";
+          tooltip += `<div class='tooltip_student'><span class='${iconClass}' style='background:${item.color}'></span>${className}${props.title}:${rate}%</div>`;
+        });
+        tooltip += `</div>`;
+        return tooltip;
+      },
+    },
+    legend: {
+      show: true,
+      top: 0,
+      left: "110px",
+      width:
+        props.showSortSelectbox && selectBarNum.value === 1 ? "70%" : "auto",
+      itemGap: 20,
+      itemHeight: 10,
+      itemWidth: 20,
+      textStyle: { fontSize: 12, color: "#333" },
+      selectedMode: true,
+      selected: legendSelected.value,
+      type: "scroll",
+      pageButtonItemGap: 5,
+      pageButtonPosition: "end",
+      orient: "horizontal",
+      alignTo: "none",
+      pageIconColor: "#999",
+      pageIconInactiveColor: "#ccc",
+      formatter: (name: string) => name.replace(/g/g, "G"),
+    } as LegendComponentOption, // 修复类型
+    // dataZoom: [dataZoom],
+    grid: {
+      top: 50,
+      left: 20,
+      right: 40,
+      bottom: 0,
+      containLabel: true,
+    },
+    yAxis: {
+      type: "value",
+      axisLabel: {
+        formatter: "{value}%",
+        textStyle: { fontSize: 14, color: "#666", fontWeight: 400 },
+      },
+    },
+    xAxis: {
+      type: "category",
+      axisLabel: {
+        interval: 0,
+        rotate: totalWidth / dataxCount < 80 ? 45 : 0,
+        textStyle: { fontSize: 14, color: "#666", fontWeight: 400 },
+        formatter: (value: string) => {
+          const singleSeriesWidth = totalWidth / dataxCount;
+          const valueWidth = value.length * 16;
+          if (valueWidth > singleSeriesWidth) {
+            if (valueWidth > 100) return value.slice(0, 6) + "...";
+            if (singleSeriesWidth < 80) return value;
+            const maxLength = Math.floor(singleSeriesWidth / 16);
+            return value.slice(0, maxLength + 3) + "...";
+          }
+          return value;
+        },
+      },
+    },
+    series,
+  };
+
+  echartInstance.setOption(option);
+
+  LabelMouseOver();
+  ShowXAxisTooltip();
+
+  // 绑定图例切换事件
+  echartInstance.off("legendselectchanged");
+  echartInstance.on("legendselectchanged", HandleLegendSelectChanged);
+
+  // 窗口 resize 监听
+  window.addEventListener("resize", handleResize);
+
+  // 点击事件
+  echartInstance.off("click");
+  echartInstance.on("click", (params: any) => {
+    if (params.seriesType === "bar") {
+      const index = params.dataIndex;
+      const name = params.name;
+      emit("HandleChartClick", index, name);
+    }
+  });
+}
+
+/** 全选复选框切换 */
+function ChangeCheckAll() {
+  ToggleChangeAll(checkAll.value);
+}
+function ToggleChangeAll(show: boolean) {
+  if (!props.showCheckBox || !echartInstance) return;
+  const legendData = legenAllList.value;
+  legendData.forEach((name) => {
+    echartInstance?.dispatchAction({
+      type: show ? "legendSelect" : "legendUnSelect",
+      name,
+    });
+  });
+  checkAll.value = show;
+  isIndeterminate.value = false;
+}
+
+/** 图例切换回调 */
+function HandleLegendSelectChanged(params: any) {
+  const selected: Record<string, boolean> = params.selected;
+  legendSelected.value = selected;
+  const allKeys = legenAllList.value;
+  const allSelected = allKeys.every((k) => selected[k]);
+  const noneSelected = allKeys.every((k) => !selected[k]);
+  const someSelected = !allSelected && !noneSelected;
+
+  if (noneSelected) checkAll.value = false;
+  if (allSelected) checkAll.value = true;
+  isIndeterminate.value = someSelected;
+
+  // 排序相关 emit
+  if (props.showSortSelectbox) {
+    const barLegendData =
+      props.data[0]?.slice(1, props.showBarLegendIndex + 1) ?? [];
+    const barSelectData = barLegendData.filter(
+      (item) => selected[String(item)],
+    );
+    selectBarNum.value = barSelectData.length;
+    if (selectBarNum.value > 1) {
+      selectValue.value = "1";
+    } else {
+      const selectedVal = barLegendData.map((item) => selected[String(item)]);
+      const selectedBarIndex = selectedVal.indexOf(true);
+      const legendData = Object.keys(selected).filter((k) => selected[k]);
+      emit("ChangeChartOrder", selectValue.value, legendData, selectedBarIndex);
+    }
+  }
+}
+
+/** 窗口大小节流 */
+const handleResize = throttle(async () => {
+  await nextTick();
+  echartInstance?.resize();
+}, 500);
+
+/** 排序切换 */
+function ChangeOrder(val: string) {
+  selectValue.value = val;
+  const barLegendData =
+    props.data[0]?.slice(1, props.showBarLegendIndex + 1) ?? [];
+  const barLegendVal = barLegendData.map(
+    (item) => legendSelected.value[String(item)],
+  );
+  const selectedBarIndex = barLegendVal.indexOf(true);
+  const legendData = Object.keys(legendSelected.value).filter(
+    (k) => legendSelected.value[k],
+  );
+  emit("ChangeChartOrder", val, legendData, selectedBarIndex);
+}
+
+// ==================== Watch ====================
+watch(
+  () => props.data,
+  () => {
+    if (props.showSortSelectbox) {
+      const legenedList =
+        props.data[0]?.slice(1, props.showBarLegendIndex + 1) ?? [];
+      const barLegendList = legenedList.filter((item) =>
+        props.legendList.includes(String(item)),
+      );
+      selectBarNum.value = barLegendList.length;
+    }
+    LoadEchart();
+  },
+  { deep: true },
+);
+
+// ==================== Lifecycle ====================
+onMounted(() => {
+  if (props.showSortSelectbox) {
+    const legenedList =
+      props.data[0]?.slice(1, props.showBarLegendIndex + 1) ?? [];
+    const barLegendList = legenedList.filter((item) =>
+      props.legendList.includes(String(item)),
+    );
+    selectBarNum.value = barLegendList.length;
+    selectValue.value = "1";
+  }
+  LoadEchart();
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener("resize", handleResize);
+  disposeEchart();
+  document
+    .querySelectorAll("#tooltipContent, #x-axis-tooltip")
+    .forEach((el) => el.remove());
+});
+</script>
+
+<style lang="scss" scoped>
+.echart_content {
+  position: relative;
+  width: 100%;
+  margin: auto;
+  min-height: 360px;
+  height: auto;
+  .is_show_all {
+    position: absolute;
+    top: -4px;
+    left: 5px;
+  }
+}
+.chart_width {
+  width: 100%;
+  height: 400px;
+}
+.title_right {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 10px;
+  position: absolute;
+  top: -6px;
+  right: 0;
+  z-index: 99;
+  .right_item {
+    font-size: 14px;
+    color: #999;
+    font-weight: 400;
+    cursor: pointer;
+    height: 30px;
+    line-height: 32px;
+    margin-right: 5px;
+    border-bottom: 2px solid #ffffff;
+  }
+  .item_active {
+    font-size: 14px;
+    color: #2e64fa;
+    font-weight: 500;
+    cursor: pointer;
+  }
+}
+</style>

+ 700 - 0
src/components/echarts/barsCharts.vue

@@ -0,0 +1,700 @@
+<template>
+  <div class="echart_content" style="position: relative;">
+    <div class="is_show_all" v-if="showCheckBox">
+      <el-checkbox
+        v-model="checkAll"
+        :indeterminate="isIndeterminate"
+        @change="handleCheckAllChange"
+      >
+        显示全部
+      </el-checkbox>
+    </div>
+    <div class="title_right" v-if="selectBarNum === 1">
+      <div
+        :class="['right_item', { item_active: selectValue === item.value }]"
+        v-for="item in selectOptions"
+        :key="item.value"
+        @click="ChangeOrder(item.value)"
+      >
+        {{ item.label }}
+      </div>
+    </div>
+    <div ref="barChartsRef" class="chart_width" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import throttle from 'lodash/throttle'
+import type { ECharts, EChartsOption, LegendComponentOption, BarSeriesOption } from 'echarts'
+import * as echarts from 'echarts'
+import { getScorePerformanceAnalysis } from '@/utils/common'
+
+// ==================== 工具函数 ====================
+/** 安全执行 Array.includes,数组为 undefined/null 直接返回 false */
+function safeIncludes<T>(arr: T[] | undefined | null, val: T): boolean {
+  if (!Array.isArray(arr)) return false
+  return arr.includes(val)
+}
+
+// ==================== 类型定义 ====================
+type DataRow = (string | number)[]
+type ChartData = DataRow[]
+
+type SelectOption = {
+  value: string
+  label: string
+}
+
+type TooltipItem = {
+  name: string
+  value: string | number
+  teacherName?: string
+  studentName?: Array<{ studentName: string }>
+}
+
+type FiveTooltipItem = {
+  list: Array<{ label: string; value: string | number }>
+  rateNum: string | number
+}
+
+// ==================== Props ====================
+interface Props {
+  showCheckBox?: boolean
+  color?: string[]
+  data: ChartData
+  title?: string
+  unit?: string
+  isClick?: boolean
+  tooltipData?: TooltipItem[][]
+  fiveTooltipData?: FiveTooltipItem[][]
+  showSortSelectbox?: boolean
+  legendWidth?: number | string
+  gridRight?: number
+  yInverse?: boolean
+  showBarLegendIndex?: number,
+  legendList?: string[]
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  showCheckBox: true,
+  color: () => [],
+  title: '',
+  unit: '',
+  isClick: false,
+  tooltipData: () => [],
+  fiveTooltipData: () => [],
+  showSortSelectbox: false,
+  legendWidth: 'auto',
+  gridRight: 60,
+  yInverse: false,
+  showBarLegendIndex: 1,
+  legendList: () => []
+})
+
+// ==================== Emits ====================
+interface Emits {
+  (e: 'HandleChartClick', index: number, name: string): void
+  (e: 'ChangeChartOrder', val: string, legendData: string[], selectedIndex: string | number): void
+}
+const emit = defineEmits<Emits>()
+
+// ==================== 响应式状态 ====================
+const barChartsRef = ref<HTMLDivElement | null>(null)
+let echartInstance: ECharts | null = null
+
+const selectValue = ref<string>('1')
+const selectOptions = ref<SelectOption[]>([
+  { value: '1', label: '默认' },
+  { value: '2', label: '升序' },
+  { value: '3', label: '降序' },
+])
+const selectBarNum = ref<number>(0)
+const checkAll = ref<boolean>(true)
+const isIndeterminate = ref<boolean>(false)
+const colors = ref<string[]>(getScorePerformanceAnalysis())
+const legendSelected = ref<Record<string, boolean>>({})
+const legenSelectList = ref<string[]>([])
+const legenAllList = ref<string[]>([])
+const dataZoomEnd = ref<number>(0)
+const dataZoomNum = ref<number>(5)
+const currentIndex = ref<number>(0)
+const isShowDataZoom = ref<boolean>(false)
+
+// ==================== 工具函数 ====================
+function disposeEchart() {
+  if (echartInstance) {
+    echartInstance.dispose()
+    echartInstance = null
+  }
+}
+
+const handleResize = throttle(async () => {
+  await nextTick()
+  echartInstance?.resize()
+}, 500)
+
+/** 反转Y轴数据计算 */
+function getReverseData() {
+  const dataList = props.data.slice(1) || []
+  const flatValues: number[] = []
+  dataList.forEach(item => {
+    const arr = item?.slice(1) ?? []
+    arr.forEach(v => {
+      const num = Number(v)
+      if (!isNaN(num)) flatValues.push(num)
+    })
+  })
+
+  if (flatValues.length === 0) return { reverseData: [] as ChartData, dataMin: 0, dataMax: 0 }
+  const dataMin = Math.min(...flatValues)
+  const dataMax = Math.max(...flatValues)
+
+  const reverseData: ChartData = []
+  dataList.forEach(item => {
+    const row: DataRow = []
+    item.forEach((val, index) => {
+      if (index === 0) {
+        row.push(val)
+      } else {
+        const valNumber = Number(isNaN(Number(val)) ? 0 : val)
+        let calcVal: number
+        if (valNumber > 0) {
+          calcVal = dataMax - valNumber + dataMin
+        } else {
+          calcVal = dataMax > dataMin ? dataMin : 0
+        }
+        row.push(calcVal)
+      }
+    })
+    reverseData.push(row)
+  })
+  reverseData.unshift([...props.data[0]])
+  return { reverseData, dataMin, dataMax }
+}
+
+/** 全选/取消全选图例 */
+function ToggleChangeAll(show: boolean) {
+  if (!props.showCheckBox || !echartInstance) return
+  const opt = echartInstance.getOption()
+  const sourceArr = opt.dataset?.[0]?.source ?? []
+  const legendData: string[] = (sourceArr[0]?.slice(1) ?? []).map((v) => String(v))
+  legendData.forEach(name => {
+    echartInstance!.dispatchAction({
+      type: show ? 'legendSelect' : 'legendUnSelect',
+      name,
+    })
+  })
+  checkAll.value = show
+  isIndeterminate.value = false
+}
+
+function handleCheckAllChange() {
+  ToggleChangeAll(checkAll.value)
+}
+
+/** 图例切换事件 */
+function handleLegendSelectChanged(params: { selected: Record<string, boolean> }) {
+  if (!echartInstance) return
+  const opt = echartInstance.getOption()
+  const sourceArr = opt.dataset?.[0]?.source ?? []
+  const legendData: string[] = (sourceArr[0]?.slice(1) ?? []).map((v) => String(v))
+  const selected = params.selected
+  legendSelected.value = selected
+
+  const allSelected = legendData.every(k => selected[k])
+  const noneSelected = legendData.every(k => !selected[k])
+  const someSelected = !allSelected && !noneSelected
+
+  if (noneSelected) checkAll.value = false
+  if (allSelected) checkAll.value = true
+  isIndeterminate.value = someSelected
+
+  // 更新 dataZoom
+  const barMinWidth = 26
+  const numSeries = Object.values(selected).filter(Boolean).length
+  const echartWidth = (props.data.length - 1) * barMinWidth * numSeries
+  const totalWidth = barChartsRef.value?.clientWidth ?? 0
+  dataZoomNum.value = Math.floor((totalWidth - 140) / (barMinWidth * numSeries))
+  dataZoomEnd.value = Math.floor((100 / Math.max(1, props.data.length - 1)) * dataZoomNum.value)
+
+  const dataZoom = {
+    start: 0,
+    end: dataZoomEnd.value,
+    type: 'slider',
+    show: true,
+    borderColor: 'transparent',
+    borderCap: 'round',
+    xAxisIndex: [0],
+    height: 8,
+    left: 20,
+    right: 20,
+    bottom: 0,
+    fillerColor: 'transparent',
+    zoomLock: true,
+    handleSize: '0',
+    handleStyle: {
+      color: '#b8b8b8',
+      borderWidth: 2,
+    },
+    backgroundColor: 'transparent',
+    showDataShadow: false,
+    showDetail: false,
+    filterMode: 'filter',
+    pageIconColor: '#999',
+    pageIconInactiveColor: '#ccc',
+  }
+  if (echartWidth > totalWidth) {
+    echartInstance.setOption({ dataZoom })
+    echartInstance.resize()
+  }
+
+  // 更新 legenSelectList 并重绘
+  legenSelectList.value = legendData.filter(item => selected[item])
+  selectValue.value = '1'
+  const legendDataKeys = Object.keys(selected).filter(k => selected[k])
+  emit('ChangeChartOrder', selectValue.value, legendDataKeys, '')
+  LoadEchart()
+}
+
+/** 排序切换 */
+function ChangeOrder(val: string) {
+  selectValue.value = val
+  const headerRow = props.data[0] ?? []
+  const legendList = headerRow.slice(1).map(v => String(v))
+  const legendVal = legendList.map(item => legendSelected.value[item])
+  const selectedIndex = legendVal.indexOf(true)
+  const legendData = Object.keys(legendSelected.value).filter(k => legendSelected.value[k])
+  emit('ChangeChartOrder', val, legendData, selectedIndex)
+}
+
+/** 初始化图表 */
+async function LoadEchart() {
+  if (!barChartsRef.value) return
+  disposeEchart()
+
+  echartInstance = echarts.init(barChartsRef.value, null, { devicePixelRatio: 2 })
+
+  // 初始化图例列表,兜底空数组
+  const headerRow = props.data[0] ?? []
+  if (props.legendList.length > 0) {
+    legenSelectList.value = [...props.legendList]
+  } else {
+    legenSelectList.value = headerRow.slice(1).map(v => String(v))
+  }
+  legenAllList.value = headerRow.slice(1).map(v => String(v))
+
+  const { reverseData, dataMin, dataMax } = getReverseData()
+  const datasetSource = props.yInverse ? reverseData : props.data
+  const dataset = { source: datasetSource }
+
+  let constColorAll = getScorePerformanceAnalysis()
+  if (props.color.length > 0) constColorAll = props.color
+
+  // 初始化图例选中状态
+  const tempLegendSelected: Record<string, boolean> = {}
+  const dims = headerRow.slice(1).map(v => String(v))
+  dims.forEach(dim => tempLegendSelected[dim] = false)
+  legenSelectList.value.forEach(item => tempLegendSelected[item] = true)
+  legendSelected.value = tempLegendSelected
+
+  checkAll.value = legenSelectList.value.length === legenAllList.value.length
+  isIndeterminate.value = legenSelectList.value.length > 0 && legenSelectList.value.length < legenAllList.value.length
+  const numSeries = Object.values(legendSelected.value).filter(Boolean).length
+  if (props.showSortSelectbox) selectBarNum.value = numSeries
+
+  const dataxCount = Math.max(0, props.data.length - 1)
+  const totalWidth = barChartsRef.value?.clientWidth ?? 0
+  const barMinWidth = 26
+  dataZoomNum.value = Math.floor((totalWidth - 140) / Math.max(1, barMinWidth * numSeries))
+  const xAxisTotal = Math.max(1, dataset.source.length - 1)
+  dataZoomEnd.value = Math.floor((100 / xAxisTotal) * dataZoomNum.value)
+  const echartWidth = (dataset.source.length - 1) * barMinWidth * numSeries
+  const singleSeriesWidth = Math.ceil(Math.max(0, totalWidth - 140) / Math.max(1, dataxCount))
+  isShowDataZoom.value = echartWidth > totalWidth
+  let realSingleSeriesWidth = singleSeriesWidth
+  if (isShowDataZoom.value) realSingleSeriesWidth = 25 * legenSelectList.value.length
+
+  const dataZoom = {
+    start: 0,
+    end: dataZoomEnd.value,
+    type: 'slider',
+    show: true,
+    borderColor: 'transparent',
+    borderCap: 'round',
+    xAxisIndex: [0],
+    height: 8,
+    left: 20,
+    right: 20,
+    bottom: 0,
+    fillerColor: 'transparent',
+    zoomLock: true,
+    handleSize: '0',
+    handleStyle: { color: '#b8b8b8', borderWidth: 2 },
+    backgroundColor: 'transparent',
+    showDataShadow: false,
+    showDetail: false,
+    filterMode: 'filter',
+    pageIconColor: '#999',
+    pageIconInactiveColor: '#ccc',
+  }
+
+  const unit = props.unit
+  const series: BarSeriesOption[] = dims.map((dim, index) => {
+    const seriesData = dataset.source.slice(1).map((dataRow, dataIndex) => {
+      const value = dataRow[index + 1]
+      const rawVal = props.yInverse ? props.data[dataIndex + 1]?.[index + 1] : value
+      return {
+        value,
+        label: {
+          show: true,
+          position: Number(value) >= 0 ? 'top' : 'bottom',
+          color: '#666',
+          fontSize: 12,
+          formatter: () => `${rawVal ?? ''}${unit}`,
+        },
+      }
+    })
+    return {
+      type: 'bar',
+      name: dim,
+      barGap: 0,
+      barCategoryGap: 30,
+      barMaxWidth: 50,
+      barMinWidth: 20,
+      itemStyle: {
+        color: constColorAll[index] ?? '#ccc',
+      },
+      data: seriesData,
+      label: {
+        show: true,
+        color: '#666',
+        fontSize: 12,
+        formatter: (params: any) => {
+          const rawVal = props.yInverse ? props.data[params.dataIndex + 1]?.[params.seriesIndex + 1] : params.value
+          return `${rawVal ?? ''}${unit}`
+        },
+      },
+    }
+  })
+
+  const option: EChartsOption = {
+    dataset,
+    tooltip: {
+      axisPointer: { type: dataset.source.length < 3 ? 'none' : 'shadow' },
+      trigger: 'axis',
+      triggerOn: 'mousemove | click',
+      renderMode: 'html',
+      confine: true,
+      extraCssText: 'border-radius: 4px;padding:5px 0px 5px 5px;white-space:normal;word-wrap:break-word;max-width: 400px;',
+      enterable: true,
+      formatter: (params: any[]) => {
+        let tooltip = `<div class='tooltip_content'>`
+        const newData = dataset.source.slice(1)
+        const xAxisName = newData[params[0].dataIndex]?.[0] ?? ''
+        let teachClassName = ''
+        if (props.tooltipData.length > 0) {
+          const tipItem = props.tooltipData[params[0].seriesIndex]?.[params[0].dataIndex]
+          teachClassName = tipItem?.teacherName ?? ''
+        }
+        tooltip += `<div class='tooltip_title'>${xAxisName} ${teachClassName}</div>`
+        params.forEach((item) => {
+          const className = item.seriesName
+          const rate = item.data.value !== undefined ? item.data.value : item.data[item.seriesIndex + 1]
+          if (props.tooltipData.length > 0) {
+            const tipItem = props.tooltipData[item.seriesIndex]?.[item.dataIndex]
+            tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:${item.color}'></span>${tipItem?.name ?? ''}:${tipItem?.value ?? ''}</div>`
+            const studentList = tipItem?.studentName?.map(s => s.studentName) ?? []
+            const studentNames = studentList.join('、')
+            if (studentNames) tooltip += `<div class='tooltip_student'><span class='tooltip_zw'></span> (${studentNames})</div>`
+          } else if (props.fiveTooltipData.length > 0) {
+            const tipItem = props.fiveTooltipData[item.dataIndex]?.[item.seriesIndex]
+            const list = tipItem?.list ?? []
+            const rateNum = tipItem?.rateNum ?? '-'
+            tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:${item.color}'></span>${className}${props.title}:${rate === '-' ? '-' : rate + unit},${rateNum}人</div>`
+            if (item === params[params.length - 1]) {
+              list.forEach(l => {
+                tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:#CCCCCC'></span>${l.label}:${l.value}</div>`
+              })
+            }
+          } else if (props.yInverse && props.tooltipData.length === 0) {
+            const sourceData = props.data
+            const dIdx = item.dataIndex + 1
+            const sIdx = item.seriesIndex + 1
+            const r = sourceData?.[dIdx]?.[sIdx] ?? '-'
+            tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:${item.color}'></span>${className}${props.title}:${r ? r + unit : '-'}</div>`
+          } else {
+            tooltip += `<div class='tooltip_student'><span class='tooltip_rect_icon' style='background:${item.color}'></span>${className}${props.title}:${rate ? rate + unit : '-'}</div>`
+          }
+        })
+        tooltip += `</div>`
+        return tooltip
+      },
+    },
+    legend: {
+      show: true,
+      top:0,
+      left: props.showCheckBox ? '110px' : 0,
+      width: props.showSortSelectbox && selectBarNum.value === 1 ? '78%' : props.legendWidth,
+      itemGap: 20,
+      itemHeight: 10,
+      itemWidth: 20,
+      textStyle: { fontSize: 12, color: '#333' },
+      selectedMode: true,
+      selected: legendSelected.value,
+      type: 'scroll',
+      pageButtonItemGap: 5,
+      pageButtonPosition: 'end',
+      orient: 'horizontal',
+      alignTo: 'none',
+      pageIconColor: '#999',
+      pageIconInactiveColor: '#ccc',
+      formatter: (name: string) => name.replace(/g/g, 'G'),
+    } as LegendComponentOption,
+    dataZoom: isShowDataZoom.value ? [dataZoom] : [],
+    grid: {
+      top: 50,
+      left: 40,
+      right: props.gridRight,
+      bottom: isShowDataZoom.value ? 15 : 0,
+      containLabel: true,
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: {
+        formatter: (val: number) => {
+          let value = props.yInverse ? dataMax - val + dataMin : val
+          value = props.yInverse ? Number(value.toFixed(0)) : Number(value.toFixed(1))
+          return unit === '%' ? `${value}${unit}` : String(value)
+        },
+        textStyle: { fontSize: 14, color: '#666', fontWeight: 400 },
+      },
+      splitArea: {
+        show: true,
+        areaStyle: { color: ['#fafafa', '#ffffff'] },
+      },
+      min: props.yInverse ? dataMin : undefined,
+      max: props.yInverse ? dataMax : undefined,
+    },
+    xAxis: {
+      type: 'category',
+      triggerEvent: true,
+      axisTick: { alignWithLabel: true },
+      axisLabel: {
+        interval: 0,
+        rotate: realSingleSeriesWidth < 80 ? 45 : 0,
+        textStyle: { fontSize: 14, color: '#666', fontWeight: 400 },
+        formatter: (value: number) => {
+          const newData = dataset.source.slice(1)
+          return String(newData[value]?.[0] ?? '')
+        },
+      },
+    },
+    series,
+  }
+
+  echartInstance.setOption(option, true)
+
+  // 图例事件
+  echartInstance.off('legendselectchanged')
+  echartInstance.on('legendselectchanged', handleLegendSelectChanged)
+  window.addEventListener('resize', handleResize)
+
+  // 点击与高亮逻辑
+  if (props.isClick) {
+    const gridRect = echartInstance.getModel().getComponent('grid').coordinateSystem.getRect()
+    const xAxisDataLength = Math.max(0, props.data.length - 1)
+    let itemBarWidth = 20
+    let calcSingleLabelWidth = xAxisDataLength === 0 ? 0 : gridRect.width / xAxisDataLength
+    let height = 0
+    if (isShowDataZoom.value) {
+      calcSingleLabelWidth = numSeries * 26 - 10
+    } else {
+      itemBarWidth = (calcSingleLabelWidth * 0.7) / numSeries > 50 ? 50 : (calcSingleLabelWidth * 0.7) / numSeries
+    }
+    const updateSeries = dims.map((dim, index) => {
+      const seriesData = dataset.source.slice(1).map((dataRow, dataIndex) => {
+        const value = dataRow[index + 1]
+        const rawVal = props.yInverse ? props.data[dataIndex + 1]?.[index + 1] : value
+        return {
+          value,
+          label: {
+            show: itemBarWidth > 23,
+            position: Number(value) >= 0 ? 'top' : 'bottom',
+            color: '#666',
+            fontSize: 12,
+            formatter: () => `${rawVal ?? ''}${unit}`,
+          },
+        }
+      })
+      return {
+        type: 'bar',
+        name: dim,
+        barGap: 0,
+        barCategoryGap: 30,
+        barMaxWidth: 50,
+        barMinWidth: 20,
+        itemStyle: { color: constColorAll[index] },
+        data: seriesData,
+        label: {
+          show: true,
+          color: '#666',
+          fontSize: 12,
+          formatter: (params: any) => {
+            const rawVal = props.yInverse ? props.data[params.dataIndex + 1]?.[params.seriesIndex + 1] : params.value
+            return `${rawVal ?? ''}${unit}`
+          },
+        },
+      } as BarSeriesOption
+    })
+    echartInstance.setOption({ series: updateSeries })
+
+    // 点击柱子
+    echartInstance.off('click')
+    echartInstance.on('click', (params: any) => {
+      if (params.seriesType === 'bar') {
+        const dataIndex = params.dataIndex
+        currentIndex.value = dataIndex
+        const pixelPosition = echartInstance!.convertToPixel({ xAxisIndex: 0 }, params.name)
+        echartInstance!.setOption({
+          graphic: {
+            id: 'highlight-box',
+            type: 'rect',
+            shape: {
+              x: pixelPosition - calcSingleLabelWidth / 2,
+              y: gridRect.y,
+              width: calcSingleLabelWidth,
+              height: gridRect.height - height,
+            },
+            style: { fill: params.color + '30' },
+          },
+        })
+        const newData = dataset.source.slice(1)
+        const name = String(newData[dataIndex]?.[0] ?? '')
+        emit('HandleChartClick', dataIndex, name)
+      }
+    })
+
+    // 默认高亮第一个
+    if (dataset.source[1]?.length > 1) {
+      const defaultPixelPosition = echartInstance.convertToPixel({ xAxisIndex: 0 }, dataset.source[1][0])
+      echartInstance.setOption({
+        graphic: {
+          id: 'highlight-box',
+          type: 'rect',
+          shape: {
+            x: defaultPixelPosition - calcSingleLabelWidth / 2,
+            y: gridRect.y,
+            width: calcSingleLabelWidth,
+            height: gridRect.height - height,
+          },
+          style: { fill: 'rgba(84,112,198,0.1)' },
+        },
+      })
+    }
+  }
+
+  // dataZoom 滚动事件
+  echartInstance.off('datazoom')
+  echartInstance.on('datazoom', (params: any) => {
+    const gridRect = echartInstance!.getModel().getComponent('grid').coordinateSystem.getRect()
+    let calcSingleLabelWidth = gridRect.width / Math.max(1, props.data.length)
+    let height = 0
+    if (isShowDataZoom.value) calcSingleLabelWidth = realSingleSeriesWidth
+
+    const idx = currentIndex.value
+    if (dataset.source.length > idx + 1) {
+      const pixelPosition = echartInstance!.convertToPixel({ xAxisIndex: 0 }, dataset.source[idx + 1][0])
+      echartInstance!.setOption({
+        graphic: {
+          id: 'highlight-box',
+          type: 'rect',
+          shape: {
+            x: pixelPosition - calcSingleLabelWidth / 2,
+            y: gridRect.y,
+            width: calcSingleLabelWidth,
+            height: gridRect.height - height,
+          },
+          style: { fill: 'rgba(84,112,198,0.1)' },
+        },
+      })
+    }
+  })
+}
+
+// ==================== 监听与生命周期 ====================
+watch(
+  () => props.data,
+  () => {
+    if (props.showSortSelectbox) {
+      // 兜底空数组,防止 undefined
+      const legenedList = props.data[0]?.slice(1, props.showBarLegendIndex + 1) ?? []
+      const barLegendList = legenedList.filter(item => safeIncludes(props.legendList, String(item)))
+      selectBarNum.value = barLegendList.length
+    }
+    LoadEchart()
+  },
+  { deep: true },
+)
+
+onMounted(() => {
+  if (props.showSortSelectbox) {
+    const legenedList = props.data[0]?.slice(1) ?? []
+    const barLegendList = legenedList.filter(item => safeIncludes(props.legendList, String(item)))
+    selectBarNum.value = barLegendList.length
+    selectValue.value = '1'
+  }
+  LoadEchart()
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', handleResize)
+  disposeEchart()
+  document.querySelectorAll('#tooltipContent, #x-axis-tooltip').forEach(el => el.remove())
+})
+</script>
+
+<style lang="scss" scoped>
+.echart_content {
+  position: relative;
+  width: 100%;
+  margin: auto;
+  min-height: 360px;
+  height: auto;
+  .is_show_all {
+    position: absolute;
+    top: -4px;
+    left: 5px;
+  }
+}
+.chart_width {
+  width: 100%;
+  height: 400px;
+}
+.title_right {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 10px;
+  position: absolute;
+  top: -6px;
+  right: 0;
+  z-index: 99;
+  .right_item {
+    font-size: 14px;
+    color: #999;
+    font-weight: 400;
+    cursor: pointer;
+    height: 30px;
+    line-height: 32px;
+    margin-right: 5px;
+    border-bottom: 2px solid #ffffff;
+  }
+  .item_active {
+    font-size: 14px;
+    color: #2E64FA;
+    font-weight: 500;
+    cursor: pointer;
+  }
+}
+</style>

+ 474 - 0
src/components/echarts/radarCharts.vue

@@ -0,0 +1,474 @@
+<template>
+  <div class="echart_content" style="overflow: hidden;">
+    <div class="is_show_all" v-if="showCheckBox">
+      <el-checkbox
+        v-model="allSeriesSelected"
+        :indeterminate="isIndeterminate"
+        @change="toggleChangeAll(allSeriesSelected)"
+      >
+        显示全部
+      </el-checkbox>
+    </div>
+    <!-- 雷达图容器 -->
+    <div
+      ref="radarChartRef"
+      class="chart_box"
+      :style="{ height: reportHeight ? reportHeight : `${height}px` }"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
+import type { ECharts, EChartsOption, RadarSeriesOption, LegendComponentOption } from "echarts";
+import * as echarts from "echarts";
+import { getGGroupColor, getScorePerformanceAnalysis } from "@/utils/common";
+
+// ====================== 类型定义 ======================
+/** G组颜色对象类型 */
+type GColorItem = {
+  name: string;
+  color: string;
+};
+
+/** 表格数据源行:[维度名称, 数值1, 数值2...] */
+type DataRow = (string | number)[];
+type ChartData = DataRow[];
+
+/** Radar 指示器配置 */
+type RadarIndicator = {
+  name: string;
+  max: number | null;
+  min: number | null;
+  axisLabel: { show: boolean; formatter?: string; textStyle?: Record<string, any> };
+  color: string;
+  nameTextStyle: { fontSize?: number; fontWeight: string };
+};
+
+// ====================== Props 定义 ======================
+interface Props {
+  showCheckBox?: boolean;
+  height?: number;
+  reportHeight?: number | string;
+  showLegend?: boolean;
+  legengAlign?: string;
+  showTooltip?: boolean;
+  showDataLabel?: boolean;
+  unit?: string;
+  legendList?: string[];
+  legendLeft?: string;
+  colorType?: "gColors" | "colors";
+  color?: string[];
+  data: ChartData;
+  isClick?: boolean;
+  showRadiusAxis?: boolean;
+  fontSize?: number | string;
+  fontColor?: string;
+  legendWidth?: number | string;
+}
+const props = withDefaults(defineProps<Props>(), {
+  showCheckBox: true,
+  height: 400,
+  reportHeight: "",
+  showLegend: true,
+  legengAlign: "110px",
+  showTooltip: true,
+  showDataLabel: false,
+  unit: "%",
+  legendList: () => [],
+  legendLeft: "110px",
+  colorType: "colors",
+  color: () => [],
+  isClick: false,
+  showRadiusAxis: true,
+  fontSize: "",
+  fontColor: "",
+  legendWidth: "auto",
+});
+
+// ====================== Emits 定义 ======================
+interface Emits {
+  (e: "HandleChartClick", index: number, name: string): void;
+}
+const emit = defineEmits<Emits>();
+
+// ====================== 响应式状态 ======================
+const radarChartRef = ref<HTMLDivElement | null>(null);
+let myChart: ECharts | null = null;
+
+const allSeriesSelected = ref<boolean>(false);
+const isIndeterminate = ref<boolean>(false);
+const gColors = ref<GColorItem[]>(getGGroupColor());
+const colors = ref<string[]>(getScorePerformanceAnalysis());
+const legendSelected = ref<Record<string, boolean>>({});
+const legenSelectList = ref<string[]>([]);
+
+// ====================== 工具方法 ======================
+/** 销毁 echarts 实例 */
+function disposeChart() {
+  if (myChart) {
+    myChart.dispose();
+    myChart = null;
+  }
+}
+
+/** 窗口自适应 */
+function resizeFn() {
+  myChart?.resize();
+}
+
+/** 切换全部图例显示/隐藏 */
+function toggleChangeAll(show: boolean) {
+  if (!props.showCheckBox || !myChart) return;
+  const opt = myChart.getOption();
+  const legendData: string[] = opt.legend[0].data;
+  legendData.forEach((name) => {
+    myChart!.dispatchAction({
+      type: show ? "legendSelect" : "legendUnSelect",
+      name,
+    });
+  });
+  allSeriesSelected.value = show;
+  isIndeterminate.value = false;
+}
+
+/** 图例切换回调 */
+function handleLegendSelectChanged(params: { selected: Record<string, boolean> }) {
+  if (!myChart) return;
+  const opt = myChart.getOption();
+  const legendData: string[] = opt.legend[0].data;
+  const selected = params.selected;
+  legendSelected.value = selected;
+
+  const allSelected = legendData.every((name) => selected[name]);
+  const noneSelected = legendData.every((name) => !selected[name]);
+  const someSelected = !allSelected && !noneSelected;
+
+  if (noneSelected) allSeriesSelected.value = false;
+  if (allSelected) allSeriesSelected.value = true;
+  isIndeterminate.value = someSelected;
+}
+
+/** 绑定雷达图指示器点击事件 */
+function bindChartNameEvents(indicators: RadarIndicator[]) {
+  if (!myChart) return;
+  myChart.on("click", (params: any) => {
+    if (params.componentType === "radar" && params.targetType === "axisName") {
+      const name = params.name as string;
+      const dataList = props.data.slice(1).map((row) => String(row[0]));
+      const index = dataList.indexOf(name);
+
+      const newIndicators = indicators.map((item, key) => ({
+        ...item,
+        color: key === index ? "#2e64fa" : "#666666",
+        nameTextStyle: {
+          ...item.nameTextStyle,
+          fontWeight: key === index ? "bold" : "normal",
+        },
+      }));
+
+      myChart.setOption({
+        radar: { indicator: newIndicators },
+      });
+      emit("HandleChartClick", index, name);
+    }
+  });
+}
+
+/** 初始化渲染雷达图 */
+function initChart() {
+  if (!radarChartRef.value) return;
+  disposeChart();
+
+  myChart = echarts.init(radarChartRef.value, null, { devicePixelRatio: 2 });
+
+  // 初始化选中图例列表,统一转为 string
+  if (props.legendList.length > 0) {
+    legenSelectList.value = [...props.legendList];
+  } else {
+    legenSelectList.value = props.data[0]?.slice(1).map((v) => String(v)) ?? [];
+  }
+
+  // 构造雷达指示器
+  const indicators: RadarIndicator[] = props.data.slice(1).map((row, index) => {
+    const name = String(row[0]);
+    return {
+      name,
+      max: props.unit === "%" ? 100 : null,
+      min: props.unit === "%" ? 0 : null,
+      axisLabel:
+        props.showRadiusAxis && index === 0
+          ? {
+              show: true,
+              formatter: `{value}${props.unit}`,
+              textStyle: {
+                fontSize: props.fontSize || 12,
+                color: "#666",
+              },
+            }
+          : { show: false },
+      color: props.isClick && index === 0 ? "#2e64fa" : props.fontColor || "#666666",
+      nameTextStyle: {
+        fontSize: props.fontSize || 14,
+        fontWeight: props.isClick && index === 0 ? "bold" : "normal",
+      },
+    };
+  });
+
+  // 图例维度名称
+  const legendData = props.data[0]?.slice(1).map((v) => String(v)) ?? [];
+
+  // 生成颜色列表
+  let colorList: string[] = [];
+  if (props.colorType === "gColors") {
+    colorList = legendData.map((item) => {
+      const findColor = gColors.value.find((g) => g.name === item);
+      return findColor?.color ?? "#848BDC";
+    });
+  } else {
+    colorList = props.color.length > 0 ? props.color : colors.value;
+  }
+
+  // 初始化图例选中状态
+  const tempLegendSelected: Record<string, boolean> = {};
+  legendData.forEach((dim) => {
+    tempLegendSelected[dim] = false;
+  });
+  legenSelectList.value.forEach((item) => {
+    tempLegendSelected[item] = true;
+  });
+  legendSelected.value = tempLegendSelected;
+
+  // 更新全选状态
+  allSeriesSelected.value = legenSelectList.value.length === legendData.length;
+  isIndeterminate.value =
+    legenSelectList.value.length > 0 && legenSelectList.value.length < legendData.length;
+
+  // 构造 series 数据
+  const seriesData = legendData.map((grade, gradeIndex) => {
+    const values = props.data.slice(1).map((row) => Number(row[gradeIndex + 1]));
+    return {
+      name: grade,
+      value: values,
+      label: {
+        show: props.showDataLabel,
+        position: "outside",
+        formatter: `{value}${props.unit}`,
+      },
+    };
+  });
+
+  // 非百分比时自动计算 min/max
+  if (props.unit !== "%") {
+    let maxValue = -Infinity;
+    let minValue = Infinity;
+    seriesData.forEach((item) => {
+      item.value.forEach((num) => {
+        maxValue = Math.max(maxValue, num);
+        minValue = Math.min(minValue, num);
+      });
+    });
+    indicators.forEach((item) => {
+      item.max = Math.ceil(maxValue);
+      item.min = Math.floor(minValue);
+    });
+  }
+
+  // ECharts Option
+  const option: EChartsOption = {
+    emphasis: { blurScope: "global" },
+    legend: {
+      show: props.showLegend,
+      data: legendData,
+      width: props.legendWidth,
+      top:0,
+      left: props.legendLeft,
+      itemGap: 20,
+      itemHeight: 10,
+      itemWidth: 20,
+      textStyle: { fontSize: 12, color: "#333" },
+      icon: "path://M352.64 526.336a158.272 158.272 0 0 0 2.56 17.664H96v-64h259.2a161.952 161.952 0 0 0-2.336 15.232A160 160 0 0 1 672 512a160.992 160.992 0 0 0-3.2-32H928v64h-259.2q1.12-5.504 1.824-11.104a160 160 0 0 1-318.016-6.4zM416 512a96 96 0 1 0 96-96 96 96 0 0 0-96 96z m254.72 20.224v-0.576z m-318.08-5.888v-1.664q-0.064 0.832 0 1.664z m-0.16-2.08v-1.248a5.44 5.44 0 0 0 0 1.248z m0-2.08v-0.928z m0-2.048zM352 518.08z m0-2.048zM352 512v-4.096V512z m0-5.344v-0.736 0.736z m0-2.56z m0.256-4.128z m0-1.664z m0-1.6v-0.992z",
+      selectedMode: true,
+      selected: legendSelected.value,
+      type: "scroll",
+      pageButtonItemGap: 5,
+      pageButtonPosition: "end",
+      orient: "horizontal",
+      alignTo: "none",
+      pageIconColor: "#999",
+      pageIconInactiveColor: "#ccc",
+    } as LegendComponentOption,
+    tooltip: {
+      show: props.showTooltip,
+      triggerOn: "mousemove | click",
+      renderMode: "html",
+      confine: true,
+      extraCssText: "border-radius: 4px;white-space:normal;word-wrap:break-word;max-width: 400px;",
+      enterable: true,
+      formatter: (params: any[]) => {
+        const opt = myChart!.getOption();
+        const radarIndicators = opt.radar[0].indicator as RadarIndicator[];
+        let tooltipContent = `<div class='tooltip_content'>`;
+        const title = params[0].name;
+        tooltipContent += `<div class='tooltip_title'>${title}</div>`;
+        radarIndicators.forEach((item, index) => {
+          if (item.name) {
+            const value = params[0].value[index];
+            tooltipContent += `<div class='tooltip_student'>${item.name}:${value}${props.unit}</div>`;
+          }
+        });
+        return tooltipContent;
+      },
+    },
+    color: colorList,
+    radar: {
+      indicator: indicators,
+      shape: indicators.length < 3 ? "circle" : undefined,
+      splitNumber: 5,
+      startAngle: 90,
+      splitLine: { show: true },
+      axisLabel: { show: false },
+      splitArea: {
+        show: true,
+        areaStyle: {
+          opacity: 0.1,
+          color: ["#f0f8ff", "#e0f7fa", "#fff3e0", "#efebe9", "#f5f5f5"],
+        },
+      },
+      radiusAxis: {
+        type: "value",
+        min: 0,
+        max: 100,
+        interval: 20,
+        splitLine: { show: true, lineStyle: { color: "#eee" } },
+        splitArea: { show: false },
+      },
+      axisLine: {
+        show: props.data.length < 3 ? false : true,
+        lineStyle: { color: "#999" },
+      },
+      center: props.reportHeight ? ["50%", "50%"] : props.showLegend ? ["50%", "55%"] : ["50%", "55%"],
+    },
+    triggerEvent: true,
+    series: [
+      {
+        type: "radar",
+        symbol: "circle",
+        symbolSize: 7,
+        data: seriesData,
+        itemStyle: { opacity: 1 },
+        emphasis: {
+          symbol: "none",
+          focus: "self",
+          itemStyle: { opacity: 1, borderWidth: 2 },
+          areaStyle: { opacity: 0.1 },
+        },
+        blur: {
+          symbol: "none",
+          itemStyle: { opacity: 0.2 },
+          lineStyle: { opacity: 0.2 },
+        },
+      } as RadarSeriesOption,
+    ],
+  };
+
+  myChart.setOption(option);
+
+  // 绑定事件
+  myChart.off("legendselectchanged");
+  myChart.on("legendselectchanged", handleLegendSelectChanged);
+
+  if (props.isClick) {
+    bindChartNameEvents(indicators);
+  }
+
+  window.addEventListener("resize", resizeFn);
+}
+
+// ====================== 监听 & 生命周期 ======================
+watch(
+  () => allSeriesSelected.value,
+  (newVal) => {
+    toggleChangeAll(newVal);
+  }
+);
+
+watch(
+  () => props.data,
+  () => {
+    initChart();
+  },
+  { deep: true }
+);
+
+onMounted(() => {
+  initChart();
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener("resize", resizeFn);
+  disposeChart();
+});
+</script>
+
+<style lang="scss" scoped>
+//图标外层公共样式
+.echart_content {
+  position: relative;
+  width: 100%;
+  margin: auto;
+  min-height: 360px;
+  height: auto;
+  .is_show_all {
+    position: absolute;
+    top: -4px;
+    left: 5px;
+  }
+
+  .chart_box {
+    width: 100%;
+    height: 400px;
+  }
+}
+
+//弹窗悬浮层样式更改
+.echart_content::-webkit-scrollbar {
+  width: 100%;
+  height: 8px;
+  /* 设置滚动条宽度为8像素 */
+  background-color: transparent;
+}
+
+/* 滑块样式 */
+.echart_content::-webkit-scrollbar-thumb {
+  background-color: #b8b8b8;
+  /* 设置滑块颜色为深灰色 */
+  border-radius: 4px;
+  /* 设置滑块边角半径为4像素 */
+  min-height: 40px;//设置手柄最小高度
+}
+
+/* 滚动条轨道内部空白区域样式 */
+.echart_content::-webkit-scrollbar-track {
+  background-color: #f0f0f0;
+  /* 设置轨道背景色为浅灰色 */
+}
+
+/* 滚动条两端按钮样式 */
+.echart_content::-webkit-scrollbar-button {
+  display: none;
+  /* 不显示按钮 */
+}
+
+/* 交叉点处的区域样式 */
+.echart_content::-webkit-scrollbar-corner {
+  background-color: transparent;
+  /* 设置交叉点处的背景色为透明 */
+}
+
+/* 调整大小手柄样式 */
+.echart_content::-webkit-resizer {
+  display: none;
+  /* 不显示调整大小手柄 */
+}
+</style>

+ 6 - 1
src/styles/element.scss

@@ -37,7 +37,12 @@
     border-color: #5883fb;
     color: #fff !important;
 } 
-
+.el-button--text{
+  color: #2e64fa;
+}
+.el-button--text:not(.is-disabled):hover, .el-button--text:not(.is-disabled):focus {
+  color: rgb(87.8, 131, 251);
+}    
 .el-button {
 
   &:hover {

+ 45 - 0
src/utils/common.ts

@@ -198,6 +198,51 @@ export const getScorePerformanceAnalysis = () => {
     "#84313D",
   ];
 }
+//获取G组 G10-G1对应的颜色值
+export const getGGroupColor= () => {
+  return [
+    {
+      name: "G1",
+      color: "#5470C6",
+    },
+    {
+      name: "G2",
+      color: "#90CB75",
+    },
+    {
+      name: "G3",
+      color: "#D3A141",
+    },
+    {
+      name: "G4",
+      color: "#EE6666",
+    },
+    {
+      name: "G5",
+      color: "#72C0DD",
+    },
+    {
+      name: "G6",
+      color: "#3BA272",
+    },
+    {
+      name: "G7",
+      color: "#FC8451",
+    },
+    {
+      name: "G8",
+      color: "#995FB3",
+    },
+    {
+      name: "G9",
+      color: "#EA7ACB",
+    },
+    {
+      name: "G10",
+      color: "#FAC858",
+    },
+  ];
+}
 // 去掉小数点后面为0的 如果不为0 最多只保留2位小数
 /**
  * 数字格式化:整数直接返回、小数最多保留2位并去除末尾多余0

+ 1 - 1
src/views/analysis/classComparison.vue

@@ -1,5 +1,5 @@
 <template>
-  <ReportModule tableOrChart="qita" :showTitle="false" :showPrintBtn="false" :showExportBtn="false"
+  <ReportModule tableOrChart="qita" :showHeader="false" :showTitle="false" :showPrintBtn="false" :showExportBtn="false"
     :showDescribe="false">
     <template #module_qita>
       <ModuleTab :tabList="state.tabList" @TabChange="TabChange"></ModuleTab>

+ 1247 - 61
src/views/analysis/groupAnalysis.vue

@@ -1,88 +1,1274 @@
 <template>
-  <!-- 成绩查询 成绩单 -->
-  <ReportModule :showTitle="false" :showDescribe="false" tableOrChart="table">
-    <template #title_left>
-      <el-input v-model="state.keyWord" style="width: 200px" placeholder="请输入学号或姓名" class="input_with">
-        <template #append>
-          <el-button :icon="Search" />
-        </template>
-      </el-input>
-      <span class="count_item">应考:293人</span>
-      <span class="count_item">实考:280人</span>
-      <span class="count_item orange">缺考:13人</span>
+  <ReportModule
+    :showTitle="true"
+    :titleList="[state.groupTitle]"
+    :showDescribe="true"
+    tableOrChart="chart"
+    :showPrintBtn="false"
+    :showExportBtn="false"
+  >
+    <template #title_right>
+      <EchartType
+        :chartTypeList="state.problemAnalysisData.chartTypeList"
+        :current="state.problemAnalysisData.chartType"
+        @ChangeEchartType="
+          (val) => ChangeEchartType(val, 'problemAnalysisData')
+        "
+      />
+    </template>
+    <template #module_table_chart>
+      <template v-if="state.problemAnalysisData.data.length > 0">
+        <BarLineCharts
+          v-if="state.problemAnalysisData.chartType == 'line_bar_chart'"
+          :legendList="state.problemAnalysisData.legendList"
+          :showBarLegendIndex="state.problemAnalysisData.showBarLegendIndex"
+          title="得分率"
+          :data="state.problemAnalysisData.data"
+          @HandleChartClick="HandleChartClick"
+          @ChangeChartOrder="
+            (sortType, legendData, barIndex) =>
+              ChangeChartOrder(sortType, legendData, barIndex, 1)
+          "
+        >
+        </BarLineCharts>
+        <BarsCharts
+          v-if="state.problemAnalysisData.chartType == 'vertical_bar'"
+          :key="state.chartKey"
+          :data="state.problemAnalysisData.data"
+          :legendList="state.problemAnalysisData.legendList"
+          :showSortSelectbox="true"
+          unit="%"
+          title="得分率"
+          :isClick="true"
+          @HandleChartClick="HandleChartClick"
+          @ChangeChartOrder="
+            (sortType, legendData, barIndex) =>
+              ChangeChartOrder(sortType, legendData, barIndex, 1)
+          "
+        >
+        </BarsCharts>
+        <RadarCharts
+          v-if="state.problemAnalysisData.chartType == 'radar_chart'"
+          :key="state.chartKey"
+          :data="state.problemAnalysisData.data"
+          :legendList="state.problemAnalysisData.legendList"
+          :showCheckBox="true"
+          :openShowAllLegend="true"
+          :isClick="true"
+          @HandleChartClick="HandleChartClick"
+        >
+        </RadarCharts>
+      </template>
+      <div
+        v-else
+        class="no_content_data"
+        v-loading="state.dataLoading"
+        :element-loading-text="state.loadingText"
+        element-loading-spinner="el-icon-loading"
+        element-loading-background="#ffffff"
+      >
+        <span>暂无数据</span>
+      </div>
     </template>
+    <template #module_describe>
+      展示每道试题的得分率图。得分率指实际得分/考核分的比值,换算成的百分数。可以用于分析每道试题的难易程度和质量,试题得分率高意味着试题难度低或者学生整体水平高;得分率低意味着试题难度较高或者考生整体水平较低。点击每道试题的柱或雷达图的题号可在下方查看该题每个班的得分率情况。
+    </template>
+  </ReportModule>
+  <ReportModule
+    :showTitle="true"
+    :titleList="[state.groupTitle, state.questionTitle]"
+    :showDescribe="true"
+    tableOrChart="chart"
+    :showPrintBtn="false"
+    :showExportBtn="false"
+  >
     <template #title_right>
-      <el-checkbox-group class="checkbox_group" v-model="state.checkList">
-        <el-checkbox label="显示分组" value="group" />
-        <el-checkbox label="显示小题" value="question" />
-      </el-checkbox-group>
+      <EchartType
+        :chartTypeList="state.questionScoreStatsData.chartTypeList"
+        :current="state.questionScoreStatsData.chartType"
+        @ChangeEchartType="
+          (val) => ChangeEchartType(val, 'questionScoreStatsData')
+        "
+      />
     </template>
     <template #module_table_chart>
-      <el-table :data="tableData" border style="width: 100%">
-        <el-table-column prop="date" label="Date" width="180" />
-        <el-table-column prop="name" label="Name" width="180" />
-        <el-table-column prop="address" label="Address" />
+      <template v-if="state.questionScoreStatsData.datax.length > 0">
+        <BarScoringRateVertical
+          v-if="state.questionScoreStatsData.chartType == 'vertical_bar'"
+          :datax="state.questionScoreStatsData.datax"
+          :datay="state.questionScoreStatsData.dataStackY"
+          :tooltipData="state.questionScoreStatsData.tooltipData"
+          :isShowMarkLine="true"
+          :average="state.questionScoreStatsData.rate"
+          :markLineData="state.questionScoreStatsData.markLineData"
+          :color="['#3BA272', '#EE6666']"
+          typeName="得分率"
+          :isClick="true"
+          @HandleChartClick="HandleQuestionScoreChartClick"
+        ></BarScoringRateVertical>
+        <DifferenceChart
+          v-if="state.questionScoreStatsData.chartType == 'difference_chart'"
+          :datax="state.questionScoreStatsData.datax"
+          :datay="state.questionScoreStatsData.datay"
+          unit="%"
+          type="1"
+          title="得分率"
+          :rate="state.questionScoreStatsData.rate"
+          @HandleChartClick="HandleQuestionScoreChartClick"
+        >
+        </DifferenceChart>
+      </template>
+      <div
+        v-else
+        class="no_content_data"
+        v-loading="state.dataLoading"
+        :element-loading-text="state.loadingText"
+        element-loading-spinner="el-icon-loading"
+        element-loading-background="#ffffff"
+      >
+        <span>暂无数据</span>
+      </div>
+    </template>
+    <template #module_describe>
+      说明:通过上面图形,可查看该题每个班的得分率情况,由图中可以看出,得分率最高为<span
+        style="color: #3ba272"
+        >{{ state.questionScoreStatsData.maxClass }}</span
+      >,最低为<span style="color: #ee6666">{{
+        state.questionScoreStatsData.minClass
+      }}</span
+      >。点击每个班级的柱可在下方查看该班各个选项/得分的人数分布。
+    </template>
+  </ReportModule>
+  <ReportModule
+    :showTitle="true"
+    :titleList="[state.groupTitle, state.questionTitle, state.classTitle]"
+    :showDescribe="true"
+    tableOrChart="qita"
+    :showPrintBtn="false"
+    :showExportBtn="false"
+  >
+    <template #module_qita>
+      <div class="content_left answer">
+        <BarChart
+          v-if="state.questionAnswerData.datax.length"
+          :datax="state.questionAnswerData.datax"
+          :datay="state.questionAnswerData.datay"
+          typeName="人数"
+          :showNuitY="false"
+          unit="人"
+          :unitX="
+            state.questionAnswerData.questionType == '单选题' ||
+            state.questionAnswerData.questionType == '多选题' ||
+            state.questionAnswerData.questionType == '判断题'
+              ? ''
+              : '分'
+          "
+          :showMarkPoint="false"
+          :isShowMarkLine="false"
+          :color="state.questionAnswerData.color"
+          :answerValue="state.questionAnswerData.answerValue"
+          :answerScore="state.questionAnswerData.answerScore"
+          :fullMark="state.questionAnswerData.fullMark"
+          :isClick="true"
+          @HandleChartClick="HandleQuestionAnswerChartClick"
+          style="height: 300px"
+        ></BarChart>
+        <div
+          v-else
+          class="module_chart no_content_data"
+          v-loading="state.dataLoading"
+          :element-loading-text="state.loadingText"
+          element-loading-spinner="el-icon-loading"
+          element-loading-background="#ffffff"
+        >
+          <span>暂无数据</span>
+        </div>
+      </div>
+      <div class="content_right answer table_42">
+        <el-table
+          border
+          :data="state.questionAnswerData.tableData"
+          stripe
+          align="left"
+          :header-row-style="HeaderRowStyle"
+        >
+          <el-table-column :label="state.classTitle" align="center">
+            <el-table-column
+              prop="title"
+              align="center"
+              label="名称"
+            ></el-table-column>
+            <el-table-column
+              prop="value"
+              align="center"
+              label="数值"
+            ></el-table-column>
+          </el-table-column>
+        </el-table>
+      </div>
+    </template>
+    <template #module_describe>
+      说明:通过柱图,可查看该题下该班所有学生的选项或试题得分情况。点击每个柱可查看该选项或分数的学生明细和答题卡答题情况。
+    </template>
+  </ReportModule>
+  <ReportModule
+    :showTitle="true"
+    :titleList="[
+      state.groupTitle,
+      state.questionTitle,
+      state.classTitle,
+      state.optionTitle,
+    ]"
+    :showDescribe="true"
+    tableOrChart="qita"
+    :showPrintBtn="false"
+    :showExportBtn="false"
+  >
+    <template #title_right>
+      <el-button class="default_button" @click="VisibleQuestionCard">
+        <img src="@/assets/icon/card_view.webp" />批量查看
+      </el-button>
+    </template>
+    <template #module_qita>
+      <div class="content_left card"></div>
+      <div class="content_right card table_42">
+        <el-table
+          border
+          :data="state.majorAnswerData.tableData"
+          stripe
+          highlight-current-row
+          align="left"
+          height="401px"
+          row-key="studentRegistrationCode"
+          :row-style="{ cursor: 'pointer' }"
+          :row-class-name="TableRowClassName"
+          @row-click="HandleRowClick"
+        >
+          <el-table-column
+            prop="studentUserName"
+            align="center"
+            label="学生"
+            show-overflow-tooltip
+          ></el-table-column>
+          <el-table-column
+            label="班级"
+            prop="className"
+            align="center"
+            min-width="70"
+            show-overflow-tooltip
+          ></el-table-column>
+          <el-table-column
+            prop="questionScore"
+            align="center"
+            label="得分"
+            width="70"
+            show-overflow-tooltip
+          ></el-table-column>
+          <el-table-column
+            prop="paperScore"
+            align="center"
+            label="成绩"
+            width="70"
+            show-overflow-tooltip
+          ></el-table-column>
+          <el-table-column align="center" label="操作">
+            <template #default="scope">
+              <el-button type="text" @click.stop="handleClick(scope.row)"
+                >查看答题卡</el-button
+              >
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </template>
+    <template #module_describe>
+      说明:点击左侧表格列可切换学生查看具体学生答题图片,点击学生姓名可在下方查看学生均衡分析,点击学生成绩可查看学生答题卡原卷。
+    </template>
+  </ReportModule>
+  <ReportModule
+    :showTitle="true"
+    :titleList="[state.groupTitle]"
+    :showDescribe="true"
+    tableOrChart="table"
+    :showPrintBtn="false"
+    :showExportBtn="true"
+    :currentPage="state.majorTableData.currentPage"
+    :pageSize="state.majorTableData.pageSize"
+    :total="state.majorTableData.total"
+    @update:pageSize="handleSizeChange"
+    @update:currentPage="handleCurrentChange"
+  >
+    <template #title_right></template>
+    <template #module_table_chart>
+      <el-table
+        :data="state.majorTableData.tableData"
+        ref="majorTable"
+        border
+        stripe
+        align="left"
+        :key="state.majorTableData.tableKey"
+      >
+        <template v-for="item in state.majorTableData.headerData">
+          <el-table-column
+            v-if="item.display"
+            :key="item.prop"
+            align="center"
+            :prop="item.prop"
+            :min-width="item.label.length > 4 ? 110 : 90"
+            :label="item.label"
+            fixed="left"
+            show-overflow-tooltip
+          >
+            <template #default="scope">
+              {{ scope.row[item.prop] || "-"
+              }}{{
+                item.prop == "estimatedScoreRate" && scope.row[item.prop]
+                  ? "%"
+                  : ""
+              }}
+            </template>
+          </el-table-column>
+        </template>
+        <template v-for="parent in state.majorTableData.changeHeaderData">
+          <el-table-column
+            v-if="parent.display"
+            align="center"
+            :prop="parent.prop"
+            :label="parent.label"
+            :key="parent.prop"
+          >
+            <template v-for="item in state.majorTableData.childHeaderData">
+              <el-table-column
+                v-if="item.display"
+                :prop="`${parent.prop}_${item.prop}`"
+                :key="`${parent.prop}_${item.prop}`"
+                align="center"
+                :label="item.label"
+                :min-width="item.label.length > 4 ? 100 : 80"
+                show-overflow-tooltip
+              >
+                <template #default="scope">
+                  <span v-if="item.prop.indexOf('Rate') > -1">{{
+                    scope.row[`${parent.prop}_${item.prop}`]
+                      ? `${scope.row[`${parent.prop}_${item.prop}`]}%`
+                      : "-"
+                  }}</span>
+                  <span
+                    v-else
+                    @click="OpenStudentDialog(item, parent, scope.row)"
+                    :class="{
+                      table_row_blue:
+                        (item.prop.indexOf('Count') > -1 ||
+                          item.prop.indexOf('Number') > -1) &&
+                        scope.row?.[`${parent.prop}_${item.prop}`] > 0,
+                    }"
+                    >{{
+                      scope.row?.[`${parent.prop}_${item.prop}`] ?? "-"
+                    }}</span
+                  >
+                </template>
+              </el-table-column>
+            </template>
+          </el-table-column>
+        </template>
       </el-table>
     </template>
   </ReportModule>
+  <!-- 批量查看小题答题卡 -->
+  <!-- <QuestionCard :subjectId="state.analysisStore.filterObject.subjectId" :questionId="state.cardQuestionId" :platformNumbers="cardRegistrationCodeList" :groupTitle="groupTitle" :groupName="groupName" :questionTitle="questionTitle" :classTitle="classTitle" :optionTitle="optionTitle" :tagActive="tagActive" :showDialog="showQuestionCardDialog" @CloseDialog="CloseQuestionCardDialog"></QuestionCard> -->
 </template>
 <script lang="ts" setup>
-import ReportModule from '@/components/ReportModule.vue';
-import { Search } from '@element-plus/icons-vue'
-import { onMounted, reactive, ref } from "vue";
-const tableData = [
-  {
-    date: '2016-05-03',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
+import ReportModule from "@/components/ReportModule.vue";
+import EchartType from "@/components/EchartType.vue";
+import { useAnalysisStore } from "@/store/analysis";
+import {
+  questionAnalysis,
+  queryAnswerListByAnswerAndScore,
+} from "@/api/analysis";
+import BarLineCharts from "@/components/echarts/barLineCharts.vue"; //柱状图折线图组合图组件
+import RadarCharts from "@/components/echarts/radarCharts.vue"; //雷达图
+import BarsCharts from "@/components/echarts/barsCharts.vue"; //多柱状图组件
+import BarScoringRateVertical from "@/components/echarts/barScoringRate_vertical.vue"; //得分率 纵向柱状图
+import DifferenceChart from "@/components/echarts/differenceChart.vue"; //率差图
+import BarChart from "@/components/echarts/barChart_answer.vue"; //单柱状图
+import { onMounted, reactive, watch, ref, nextTick } from "vue";
+import { cloneDeep } from "lodash-es";
+const analysisStore = useAnalysisStore();
+const state = reactive({
+  questionGroupDefault: [
+    {
+      name: "小题分析",
+      code: "problem",
+    },
+    {
+      name: "题型分析",
+      code: 11,
+    },
+    {
+      name: "错题分析",
+      code: "errors",
+    },
+    {
+      name: "客观题分析",
+      code: "selectQuestion",
+    },
+    {
+      name: "命题分析",
+      code: "proposition",
+    },
+  ],
+  questionGroupList: [], //试题分组标签 动态接口获取
+  tagActive: "problem", //选择的试题分组标签
+  groupTitle: "小题分析",
+  groupPreviousTitle: "",
+  knowledgeLayeredTitle: "", //知识点分层标题
+  groupName: "", // 分组名称
+  questionTitle: "", //题目名称
+  classTitle: "", //班级名称
+  optionTitle: "", //选项名称
+  studentUserName: "", //学生名称
+  studentRegistrationCode: "", //学生code
+  questionTypesData: {
+    chartKey: 0,
+    refresh: false,
+    data: [],
+    tableData: [],
   },
-  {
-    date: '2016-05-02',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
+  problemAnalysisData: {
+    chartType: "line_bar_chart", //默认显示折线图柱状图line_bar_chart
+    chartTypeList: [
+      {
+        label: "组合图",
+        value: "line_bar_chart",
+      },
+      {
+        label: "柱状图",
+        value: "vertical_bar",
+      },
+      {
+        label: "雷达图",
+        value: "radar_chart",
+      },
+    ],
+    legendList: [],
+    defaultLegendList: [],
+    showBarLegendIndex: 1,
+    questionList: [], // 题目列表
+    questionTableList: [], // 小题分析table
+    groupList: [], // 分组题目列表
+    groupTableList: [], // 分组table
+    headerList: [],
+    changeHeaderList: [],
+    childHeaderList: [],
+    questionListIndex: 0, //题目 索引
+    data: [], //柱状图
+  }, //小题分析数据
+  majorQuestionData: {
+    chartType: "line_bar_chart", //默认显示折线图柱状图line_bar_chart
+    legendList: [],
+    defaultLegendList: [],
+    showBarLegendIndex: 1,
+    questionListIndex: 0, //题目 索引
+    data: [], //柱状图
+  }, //分组题目 对应的题目列表
+  questionScoreStatsData: {
+    checked: false,
+    isIndeterminate: true,
+    markLineData: [], //平均分
+    chartTypeList: [
+      {
+        label: "柱状图",
+        value: "vertical_bar",
+      },
+      {
+        label: "率差图",
+        value: "difference_chart",
+      },
+    ],
+    chartType: "vertical_bar", //默认显示率差图vertical_bar
+    datax: [], //x轴数据
+    datay: [], //Y轴数据
+    dataStackY: [], //Y轴数据
+    tooltipData: [], //
+    rate: 60, //中间x的刻度线的值 数字类型
+    maxClass: "", // 得分率最高班
+    minClass: "", // 得分率最低班
+  }, //小题分析 /第N题
+  questionAnswerData: {
+    datax: [], //x轴数据
+    datay: [], //Y轴数据
+    color: "#5470C6",
+    answerValue: "", //正确答案 柱状图显示绿色
+    answerScore: [], //答案所能得的分数
+    questionType: "", //类型
+    fullMark: "", //满分
+    //   average:0,//平均分辅助线
+    tableData: [],
+    classListIndex: 0, // 索引
+  }, //小题分析 /第N题 / N班
+  majorAnswerData: {
+    paperUrl: "", //试卷地址
+    tableData: [], //表格数据
+    rowIndex: 0,
+  }, // 通过答案或者分数查询某题作答情况
+  majorStudentData: {
+    tableData: [],
+    legendList: [],
+    radarChartData: [],
+    excellentCourseList: "", //优秀科目
+    inferiorCourseList: "", //弱势科目
+  }, //科目得分率
+  majorTableData: {
+    tableKey: 0,
+    tableData: [], //小题分析表数据
+    allTableData: [],
+    headerData: [], //固定表头数据
+    changeHeaderData: [], //动态表头数据
+    childHeaderData: [], //子级表头数据
+    pageSize: 10, //每页显示数据
+    total: 0, //总数
+    currentPage: 1, //当前页
+  }, //小题分析表 + 题目分组分析
+  errorQuestionData: {
+    tableKey: 0,
+    allData: [], //
+    gradeValue: "", //选中的年级
+    className: "", //选中的年级名称
+    type: "",
+    gradeData: [], //年级下拉选项
+    tableData: [], //错题分析表数据
+  }, //错题分析表
+  studentPreviousExamData: {
+    data: null,
+    chartType: "line_chart",
+    selectList: [
+      {
+        value: "standardScore",
+        name: "标准分",
+      },
+      {
+        value: "scoreRate",
+        name: "得分率",
+      },
+      {
+        value: "classRank",
+        name: "班排",
+      },
+      {
+        value: "schoolRank",
+        name: "校排",
+      },
+      {
+        value: "examRank",
+        name: "联排",
+      },
+    ],
+    selectVal: "standardScore",
+    selectName: "标准分",
+    lineChartData: {
+      datax: [],
+      datay: [],
+      title: [],
+      tooltipData: [],
+    },
+    barChartData: {
+      //柱状图
+      legendList: [],
+      data: [],
+    },
+  }, //学生成绩历次考试分析数据
+  groupPrevious: {
+    loading: false,
+    examNameList: [],
+    examChartList: [],
+    selectedLegendList: [],
+    examChartIndex: [],
+    headerList: [],
+    dataList: [],
+  }, //题目分组历次分析(图表数据)
+  stuGroupPreviousChart: {
+    examNameList: [],
+    examChartList: [],
+    examChartIndex: [],
+  }, //学生组块历次图
+  stuGroupPreviousChartStuInfo: {
+    studentName: "", //学生姓名搜索条件
+    studentCodeList: [],
+  }, //学生历次分析图 搜索学生信息
+  stuGroupPrevious: {
+    total: 0,
+    pageNum: 1,
+    pageSize: 10,
+    headerList: [],
+    tableList: [],
+  }, //题目分组历次分析学生数据表(分页)
+  classNameList: [],
+  showHeaderSet: false, //是否显示设置表头
+  showBenchTaskSelect: false, //历次考试弹框
+  exportLoading: false, // 题型导出loading
+  exportErrorLoading: false, // 错题导出loading
+  chartKey: 0,
+
+  dataLoading: false,
+  loadingText: "加载中,请稍后……",
+
+  errorPdfLoading: false, //错题导出PDF loading
+  paperInfo: {
+    examPaperId: "", //考试科目id
+    platformNumber: "", //学籍号平台号
+    questionId: "", //题目id
+  }, //学生试卷信息
+  paperTitle: "", //学生试卷标题
+  showStudentPaperDialog: false, //是否显示学生答题卡弹窗
+  paperInfos: {
+    examPaperId: "", //考试科目id
+    platformNumber: "", //学籍号平台号
+    questionId: "", //题目id
   },
-  {
-    date: '2016-05-04',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
+  knowledgeInputDialog: false,
+  dialogData: {
+    apiName: "",
+    showDialog: false,
+    title: "",
+    tableTitle: "",
+    fiveRateName: "", //当前选择的五率的名称
+    selectSubjectName: "", //当前选择的科目名称
+    selectSchoolLevel: "", //当前选择的学校级别:0-联考 1-学校分组 2-具体学校
+    selectSchoolName: "", //当前选择的学校名称:联校 组合学校 单校名称
+    selectClassLevel: "", //当前选择的班级级别:0-年级 1-组合班级 2-具体班级
+    selectClassName: "", //当前选择的班级名称:年级 组合班级 单班
+    questionId: "", //试题ID
+    isPaper: "", //是否为全卷0-非1-是全卷
+    quesScoreType: "", //选择的类型0-满分人数 1-0分人数 3-优秀率、良好率等
+    knowledgeId: "", //如果选择的是知识点,则需要返回知识点ID
+    questionGroupName: "", //试题分组的名称
+    groupTitle: "",
   },
-  {
-    date: '2016-05-01',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
+  showQuestionCardDialog: false,
+  cardQuestionId: "", // 批量查看答题卡试题id
+  cardRegistrationCodeList: [], // 批量查看答题卡学生账号数组
+});
+const majorTable = ref(null);
+//排序
+const ChangeChartOrder = (sortType, legendData, barIndex, number) => {
+  const isHasEstimatedScore = state.problemAnalysisData.headerList.find(
+    (item) => item.prop == "estimatedScore",
+  ); //是否存在预估满分
+  const fullVolume = state.problemAnalysisData.questionList.filter(
+    (item) => item.showCode == 999,
+  ); //全卷
+  const questionList = state.problemAnalysisData.questionList.filter(
+    (item) => item.showCode != 999,
+  );
+  const newBarIndex = isHasEstimatedScore ? barIndex - 1 : barIndex;
+  if (sortType == "2") {
+    //从低到高
+    questionList.sort(function (a, b) {
+      return (
+        (a?.classList?.[newBarIndex]?.questionStats?.scoreRate || 0) -
+        (b?.classList?.[newBarIndex]?.questionStats?.scoreRate || 0)
+      );
+    });
+  } else if (sortType == "3") {
+    //从高到低
+    questionList.sort(function (a, b) {
+      return (
+        (b?.classList?.[newBarIndex]?.questionStats?.scoreRate || 0) -
+        (a?.classList?.[newBarIndex]?.questionStats?.scoreRate || 0)
+      );
+    });
+  } else {
+    //默认排序 按题号排序
+    questionList.sort(function (a, b) {
+      return (a?.showCode || 0) - (b.showCode || 0);
+    });
+  }
+  state.problemAnalysisData.questionList = [...questionList, ...fullVolume];
+  let chartData = [],
+    chartTitle = [];
+  state.problemAnalysisData.questionList.forEach((ques) => {
+    const scoreRateArr = ques.classList.map(
+      (item) => item?.questionStats?.scoreRate || 0,
+    );
+    if (isHasEstimatedScore) {
+      chartData.push([
+        ques.questionName,
+        ques.estimatedScoreRate,
+        ...scoreRateArr,
+      ]);
+    } else {
+      chartData.push([ques.questionName, ...scoreRateArr]);
+    }
+  });
+  const newChangeHeaderList = isHasEstimatedScore
+    ? [
+        { ...isHasEstimatedScore, isG: false, type: 1 },
+        ...state.problemAnalysisData.changeHeaderList,
+      ]
+    : [...state.problemAnalysisData.changeHeaderList];
+  newChangeHeaderList.forEach((item) => {
+    chartTitle.push(item.label);
+  });
+  chartTitle.unshift("group");
+  chartData.unshift(chartTitle); // 柱状图 图例标题
+  chartData.pop(); // 删除最后一行 全卷
+  state.problemAnalysisData.data = chartData;
+  state.problemAnalysisData.legendList = legendData;
+  //小题分析 /第N题
+  GetQuestionStatsData(0);
+};
+//试题图表切换公共方法
+const ChangeEchartType = (value, prop) => {
+  if (prop == "problemAnalysisData") {
+    ChangeChartOrder("1", state.problemAnalysisData.defaultLegendList, "", 1);
+  }
+  state[prop].chartType = value;
+};
+//获取小题分析数据
+const GetQuestionAnalysisData = () => {
+  state.problemAnalysisData.data = []; //柱状图折线图 X轴
+  state.dataLoading = true;
+  questionAnalysis({
+    ...analysisStore.filterObject,
+    analysisType: 0, //0-小题分析 1大题分析 2-知识点 3-能力点 question_group_code(4,5,6,7,8) 11-题型分析
+  })
+    .then((res) => {
+      if (
+        res.code == 200 &&
+        res.data &&
+        res.data.questionList &&
+        res.data.questionList.length
+      ) {
+        const { data } = res;
+        const questionList = data.questionList || [];
+        const changeHeaderList = data.changeHeaderList || []; //柱状图折线图 班级名称
+        state.problemAnalysisData.groupList = [];
+        state.problemAnalysisData.groupTableList = [];
+        state.problemAnalysisData.questionList = questionList;
+        state.problemAnalysisData.questionTableList = cloneDeep(questionList);
+        state.problemAnalysisData.headerList = data.headerList || [];
+        state.problemAnalysisData.changeHeaderList = changeHeaderList;
+        state.problemAnalysisData.childHeaderList = data.childHeaderList || [];
+
+        state.chartKey++;
+        //Y轴数据
+        let chartData = [];
+        let legendList = [];
+        const isHasEstimatedScore = state.problemAnalysisData.headerList.find(
+          (item) => item.prop == "estimatedScore",
+        ); //是否存在预估满分
+        chartData = questionList.map((item) => {
+          return isHasEstimatedScore
+            ? [item.questionName, item.estimatedScoreRate]
+            : [item.questionName];
+        });
+        questionList.forEach((ques, key) => {
+          const scoreRateArr = ques.classList.map(
+            (item) => item?.questionStats?.scoreRate || 0,
+          );
+          chartData[key].push(...scoreRateArr);
+        });
+        let chartTitle = [],
+          titleType = [],
+          classSelectLegend = [];
+        const newChangeHeaderList = isHasEstimatedScore
+          ? [
+              { ...isHasEstimatedScore, isG: false, type: 1 },
+              ...changeHeaderList,
+            ]
+          : [...changeHeaderList];
+        newChangeHeaderList.forEach((item) => {
+          chartTitle.push(item.label);
+          titleType.push(item.type ? item.type : ""); //1柱状图 2折线
+          if (
+            item.prop != "0" &&
+            item.prop != "estimatedScore" &&
+            item.prop.indexOf("school_group") == -1
+          ) {
+            classSelectLegend.push(item.label);
+          }
+        });
+        chartTitle.unshift("group");
+        if (analysisStore.filterObject.classLevel != 2) {
+          //0 年级 1 组合班级 2具体班级
+          let startIndex = 1,
+            endIndex = 3;
+          if (analysisStore.filterObject.schoolLevel == 2) {
+            //0-联考 1-学校分组 2-具体学校
+            startIndex =
+              titleType.lastIndexOf(1) > -1 ? titleType.lastIndexOf(1) + 1 : 1;
+            endIndex = titleType.indexOf(2) > -1 ? titleType.indexOf(2) + 2 : 3; //单校时显示联校、组合校、年级是柱子;班级组合、班级是折线。
+          } else {
+            //联校 组合学校 默认当前的为第一个选中的柱子
+            const barFirtIndex =
+              titleType.lastIndexOf(1) > -1 ? titleType.lastIndexOf(1) + 1 : 1;
+            startIndex = barFirtIndex;
+            endIndex = barFirtIndex + 2;
+          }
+          legendList =
+            chartTitle.length > 1 ? chartTitle.slice(startIndex, endIndex) : [];
+          state.problemAnalysisData.showBarLegendIndex = endIndex - 2;
+        } else {
+          //单班 联校和组合学校不默认显示
+          legendList = classSelectLegend;
+          state.problemAnalysisData.showBarLegendIndex =
+            changeHeaderList.length - 1;
+        }
+        chartData.unshift(chartTitle); // 柱状图 图例标题
+        chartData.pop(); // 删除最后一行 全卷
+        state.problemAnalysisData.data = chartData;
+        state.problemAnalysisData.legendList = legendList;
+        state.problemAnalysisData.defaultLegendList = cloneDeep(legendList);
+        //小题分析 /第N题
+        GetQuestionStatsData(0);
+        // 获取项目分析表
+        GetMajorTableData();
+      } else {
+        state.problemAnalysisData.groupList = [];
+        state.problemAnalysisData.groupTableList = [];
+        state.problemAnalysisData.questionList = [];
+        state.problemAnalysisData.questionTableList = [];
+        state.problemAnalysisData.showBarLegendIndex = 1;
+        state.problemAnalysisData.headerList = [];
+        state.problemAnalysisData.changeHeaderList = [];
+        state.problemAnalysisData.childHeaderList = [];
+
+        state.majorQuestionData.data = [];
+        state.majorQuestionData.legendList = [];
+
+        state.questionScoreStatsData.datax = [];
+        state.questionScoreStatsData.datay = [];
+        state.questionScoreStatsData.dataStackY = [];
+        state.questionScoreStatsData.tooltipData = [];
+        state.questionScoreStatsData.rate = 0;
+        state.questionScoreStatsData.maxClass = "";
+        state.questionScoreStatsData.minClass = "";
+
+        state.questionAnswerData.datax = [];
+        state.questionAnswerData.datay = [];
+        state.questionAnswerData.answerValue = "";
+        state.questionAnswerData.answerScore = [];
+        state.questionAnswerData.questionType = "";
+        state.questionAnswerData.fullMark = "";
+        state.questionAnswerData.tableData = [];
+        state.questionAnswerData.classListIndex = 0;
+
+        state.majorAnswerData.tableData = [];
+        state.majorAnswerData.paperUrl = "";
+        state.majorAnswerData.rowIndex = 0;
+
+        state.majorStudentData.tableData = [];
+        state.majorStudentData.legendList = [];
+        state.majorStudentData.radarChartData = [];
+        state.majorStudentData.excellentCourseList = "";
+        state.majorStudentData.inferiorCourseList = "";
+
+        state.majorTableData.tableData = [];
+        state.majorTableData.headerData = [];
+        state.majorTableData.changeHeaderData = [];
+        state.majorTableData.childHeaderData = [];
+        state.majorTableData.pageSize = 10;
+        state.majorTableData.total = 0;
+        state.majorTableData.currentPage = 1;
+      }
+    })
+    .finally(() => {
+      state.dataLoading = false;
+    });
+};
+// 点击柱状图折线图
+const HandleChartClick = (index, name) => {
+  GetQuestionStatsData(index); //小题分析
+};
+// 获取小题分析 /第N题
+const GetQuestionStatsData = (index) => {
+  const questionData = cloneDeep(state.problemAnalysisData.questionList[index]);
+  state.problemAnalysisData.questionListIndex = index;
+  state.questionTitle = questionData?.questionName || "";
+  if (questionData) {
+    state.questionScoreStatsData.datax = []; // x轴数据
+    state.questionScoreStatsData.datay = []; // Y轴数据
+    state.questionScoreStatsData.dataStackY = []; // Y轴数据
+    state.questionScoreStatsData.tooltipData = []; // 提示框内容
+    let dataStackY = [];
+    const isHasEstimatedScore = state.problemAnalysisData.headerList.find(
+      (item) => item.prop == "estimatedScore",
+    ); //是否存在预估满分
+    if (questionData.classList.length > 1) {
+      const key = state.problemAnalysisData.showBarLegendIndex;
+      state.questionScoreStatsData.markLineData = []; //辅助线
+      const classList = questionData?.classList || [];
+      if (
+        analysisStore.filterObject.classLevel == 0 ||
+        analysisStore.filterObject.classLevel == 1
+      ) {
+        //0 年级 1 组合班级 2具体班级
+        state.questionScoreStatsData.rate = Number(
+          classList?.[key - 1]?.questionStats?.scoreRate || 0,
+        );
+      } else {
+        state.questionScoreStatsData.rate = Number(
+          classList?.[key]?.questionStats?.scoreRate || 0,
+        );
+      }
+      //辅助线
+      classList.forEach((item, index) => {
+        const keyIndex =
+          analysisStore.filterObject.classLevel == 0 ||
+          analysisStore.filterObject.classLevel == 1
+            ? key - (isHasEstimatedScore ? 2 : 1)
+            : key;
+        if (index <= keyIndex) {
+          const rate = Number(item?.questionStats?.scoreRate || 0);
+          state.questionScoreStatsData.markLineData.push({
+            legendName: item.groupName,
+            value: rate,
+            isShow: index == keyIndex ? true : false, //是否显示
+          });
+        }
+      });
+      questionData.classList = questionData.classList.slice(
+        isHasEstimatedScore ? key - 1 : key,
+      ); // 取折线图数据
+    } else {
+      state.questionScoreStatsData.rate = 60;
+    }
+
+    questionData.classList.forEach((item) => {
+      state.questionScoreStatsData.datax.push(item.groupName);
+      state.questionScoreStatsData.datay.push(item.questionStats.scoreRate);
+      dataStackY.push(item.questionStats.lossRate);
+      state.questionScoreStatsData.tooltipData.push({
+        list: [
+          {
+            name: "失分率",
+            value: `${item.questionStats.lossRate}%`,
+          },
+        ],
+      });
+    });
+    // 获取最大值
+    const max = Math.max(...state.questionScoreStatsData.datay);
+    // 获取最小值
+    const min = Math.min(...state.questionScoreStatsData.datay);
+    const maxValue = max.toFixed(2);
+    const minValue = min.toFixed(2);
+
+    let maxClass = [],
+      minClass = [];
+    state.questionScoreStatsData.datay.forEach((item, index) => {
+      if (Number(item) == Number(maxValue)) {
+        maxClass.push(state.questionScoreStatsData.datax[index]);
+      }
+      if (Number(item) == Number(minValue)) {
+        minClass.push(state.questionScoreStatsData.datax[index]);
+      }
+    });
+
+    state.questionScoreStatsData.minClass = minClass.join("、");
+    state.questionScoreStatsData.maxClass = maxClass.join("、");
+    state.questionScoreStatsData.dataStackY = [
+      state.questionScoreStatsData.datay,
+      dataStackY,
+    ];
+  }
+  console.log("打印questionScoreStatsData1111", state.questionScoreStatsData);
+  // 获取小题分析 /第N题 / 第N班
+  GetQuestionAnswerData(state.problemAnalysisData.showBarLegendIndex);
+};
+//切换 获取小题分析 /第N题 获取第N班答题列表
+const HandleQuestionScoreChartClick = (index) => {
+  GetQuestionAnswerData(index + state.problemAnalysisData.showBarLegendIndex);
+};
+// 获取小题分析 /第N题 / 第N班 选项
+const GetQuestionAnswerData = (index) => {
+  const isHasEstimatedScore = state.problemAnalysisData.headerList.find(
+    (item) => item.prop == "estimatedScore",
+  ); //是否存在预估满分
+  const newINdex = isHasEstimatedScore ? index - 1 : index;
+  const classList =
+    state.problemAnalysisData.questionList?.[
+      state.problemAnalysisData.questionListIndex
+    ]?.classList?.[newINdex];
+  // console.log("打印获取小题分析班级列表",classList)
+  state.questionAnswerData.answerValue =
+    state.problemAnalysisData.questionList[
+      state.problemAnalysisData.questionListIndex
+    ].answerValue; //正确答案
+
+  state.questionAnswerData.questionType =
+    state.problemAnalysisData.questionList[
+      state.problemAnalysisData.questionListIndex
+    ].questionType; //类型
+  state.questionAnswerData.fullMark =
+    state.problemAnalysisData.questionList[
+      state.problemAnalysisData.questionListIndex
+    ].fullMark; //满分
+
+  state.questionAnswerData.classListIndex = newINdex;
+  if (classList) {
+    const answerList = classList.questionStats.answerList || [];
+    state.classTitle = classList.groupName;
+    state.questionAnswerData.datax = []; // x轴数据
+    state.questionAnswerData.datay = []; // Y轴数据
+    let sum = 0;
+    answerList.forEach((item) => {
+      sum += item.studentNum;
+      state.questionAnswerData.datax.push(item.name);
+      state.questionAnswerData.datay.push(item.studentNum);
+      state.questionAnswerData.answerScore.push(item.score);
+    });
+    state.questionAnswerData.tableData = [
+      {
+        title: "得分率",
+        value: `${classList.questionStats.scoreRate}%`,
+      },
+      {
+        title: "平均分",
+        value: `${classList.questionStats.averageScore}`,
+      },
+      {
+        title: "最高分",
+        value: `${classList.questionStats.maxScore}`,
+      },
+      {
+        title: "最低分",
+        value: `${classList.questionStats.minScore}`,
+      },
+      {
+        title: "人数",
+        value: `${sum}人`,
+      },
+    ];
+    if (analysisStore.filterObject.schoolLevel == 2) {
+      //单校时展示
+      // 获取答题情况
+      GetAnswerListByAnswerAndScore(
+        0,
+        answerList && answerList[0] ? answerList[0].name : "",
+      );
+    }
+  }
+};
+// 点击柱状图小题分析 /第N题 / 第N班 选项 获取答题情况
+const HandleQuestionAnswerChartClick = (index, name) => {
+  if (analysisStore.filterObject.schoolLevel == 2) {
+    //单校时展示
+    GetAnswerListByAnswerAndScore(index, name);
+  }
+};
+//通过答案或者分数查询某题作答情况
+const GetAnswerListByAnswerAndScore = (index, name) => {
+  state.optionTitle = name; //选项名称
+  const question =
+    state.problemAnalysisData.questionList[
+      state.problemAnalysisData.questionListIndex
+    ];
+  const classItem = question.classList[state.questionAnswerData.classListIndex];
+  const classItemKeys = Object.keys(classItem);
+  const reportParam = {
+    ...analysisStore.filterObject,
+  };
+  Object.keys(analysisStore.filterObject).forEach((item) => {
+    if (classItemKeys.indexOf(item) > -1) {
+      reportParam[item] = classItem[item];
+    }
+  });
+  const answer =
+    question.classList[state.questionAnswerData.classListIndex].questionStats
+      .answerList[index];
+  const params = {
+    ...reportParam,
+    questionId: question.questionId, // 试题id
+    registrationCodeList: answer?.registrationCodeList || [], // 学生账号数组
+  };
+  state.cardQuestionId = params.questionId; // 批量查看答题卡试题id
+  state.cardRegistrationCodeList = params.registrationCodeList; // 批量查看答题卡学生账号数组
+  queryAnswerListByAnswerAndScore(params).then((res) => {
+    if (res.code == 200) {
+      state.majorAnswerData.tableData = res.data || [];
+    } else {
+      state.majorAnswerData.tableData = [];
+    }
+  });
+};
+// 获取项目分析表
+const GetMajorTableData = () => {
+  state.majorTableData.tableKey += 1;
+  state.majorTableData.headerData = state.problemAnalysisData.headerList;
+  state.majorTableData.changeHeaderData =
+    state.problemAnalysisData.changeHeaderList;
+  state.majorTableData.childHeaderData =
+    state.problemAnalysisData.childHeaderList;
+  const headerPropData = state.problemAnalysisData.headerList.map(
+    (item) => item.prop,
+  ); //表头字段名
+  const childHeaderPropData = state.problemAnalysisData.childHeaderList.map(
+    (item) => item.prop,
+  ); //动态表头字段名
+  let allTableData = [];
+  const allList = state.problemAnalysisData.questionTableList;
+  allList.forEach((item) => {
+    let itemObj = {
+      questionId: item?.questionId || "",
+      knowledgeId: item?.knowledgeId || "",
+    };
+    const classList = item.classList;
+    headerPropData.forEach((title) => {
+      itemObj[title] = Array.isArray(item[title])
+        ? item[title].join("、")
+        : item[title];
+    });
+
+    classList.forEach((el) => {
+      if (
+        el.questionStats?.headDataBOList &&
+        el.questionStats.headDataBOList.length > 0
+      ) {
+        el.questionStats.headDataBOList.forEach((bo) => {
+          el.questionStats[`${bo.name}Rate`] = bo.rate;
+          el.questionStats[`${bo.name}StudentNumber`] = bo.studentNumber;
+        });
+      } else {
+        el.questionStats = [];
+      }
+      childHeaderPropData.forEach((field) => {
+        itemObj[`${el.groupId}_${field}`] = el.questionStats[field];
+      });
+    });
+    allTableData.push(itemObj);
+  });
+  state.majorTableData.total = allTableData.length; //总条数
+  state.majorTableData.allTableData = allTableData;
+  GetPageMajorTableData();
+  //重置表格滚动条位置
+  ResetTableScroll(); //重置表格滚动条位置
+};
+const GetPageMajorTableData = () => {
+  const start =
+    (state.majorTableData.currentPage - 1) * state.majorTableData.pageSize;
+  const end = start + state.majorTableData.pageSize;
+  state.majorTableData.tableData = state.majorTableData.allTableData.slice(
+    start,
+    end,
+  );
+};
+//重置表格滚动条位置
+const ResetTableScroll = () => {
+  nextTick(() => {
+    if (majorTable.value) {
+      const tableBody = majorTable.value.querySelector(
+        ".el-table__body-wrapper",
+      );
+      if (tableBody) {
+        tableBody.scrollTop = 0; //清除纵向滚动条位置
+        tableBody.scrollLeft = 0; // 清除横向滚动条位置
+      }
+    }
+  });
+};
+const handleCurrentChange = (val) => {
+  state.majorTableData.currentPage = val;
+  GetMajorTableData(); //加载分析表格数据
+};
+const handleSizeChange = (val: number) => {
+  state.majorTableData.pageSize = val;
+  state.majorTableData.currentPage = 1;
+  GetMajorTableData(); //加载分析表格数据
+}
+//设置表头样式
+const HeaderRowStyle = ({ row, rowIndex }) => {
+  if (rowIndex === 1) {
+    return {
+      display: "none",
+    };
+  }
+};
+const pageInit = () => {
+  state.problemAnalysisData.chartTypeList =
+    analysisStore.filterObject.classLevel != 2
+      ? [
+          {
+            label: "组合图",
+            value: "line_bar_chart",
+          },
+          {
+            label: "柱状图",
+            value: "vertical_bar",
+          },
+          {
+            label: "雷达图",
+            value: "radar_chart",
+          },
+        ]
+      : [
+          {
+            label: "柱状图",
+            value: "vertical_bar",
+          },
+          {
+            label: "雷达图",
+            value: "radar_chart",
+          },
+        ];
+  state.problemAnalysisData.chartType =
+    analysisStore.filterObject.classLevel != 2
+      ? "line_bar_chart"
+      : "vertical_bar";
+  state.questionScoreStatsData.chartType = "vertical_bar";
+  state.majorTableData.currentPage = 1;
+  state.majorAnswerData.rowIndex = 0;
+  state.studentPreviousExamData.selectVal = "standardScore";
+  state.studentPreviousExamData.selectName = "标准分";
+  GetQuestionAnalysisData(); //获取小题分析数据
+};
+// 监听筛选条件
+watch(
+  () => analysisStore.filterObject,
+  async () => {
+    pageInit();
   },
-]
-const state = reactive({
-  keyWord: '',
-  checkList: ['group']
+  { deep: true },
+);
+
+onMounted(() => {
+  pageInit();
 });
-onMounted(() => { });
 </script>
 
 <style lang="scss" scoped>
-.input_with {
-  margin-right: 10px;
+.content_left {
+  &.answer {
+    width: calc(100% - 220px);
+  }
+
+  &.card {
+    width: calc(100% - 480px);
+  }
 }
 
-.count_item {
-  font-weight: 400;
-  font-size: 16px;
-  color: #333333;
-  line-height: 24px;
-  margin-left: 10px;
+.content_right {
+  height: 100%;
+  display: flex;
+  align-items: center;
+  margin: auto;
+  padding-bottom: 0;
 
-  &.orange {
-    color: #FB9F34;
+  &.answer {
+    width: 200px;
+  }
+
+  &.card {
+    width: 460px;
   }
-}
 
-.checkbox_group {
-  :deep(.el-checkbox) {
-    margin-right: 10px;
+  :deep(.el-table) {
+    border-left: 0px;
+    border-right: 0px;
 
-    &:nth-child(1) {
-      margin-right: 20px;
+    .el-table__cell {
+      height: 44px;
     }
   }
 }
-</style>
+</style>

+ 1246 - 60
src/views/analysis/questionAnalysis.vue

@@ -1,87 +1,1273 @@
 <template>
-  <!-- 成绩查询 成绩单 -->
-  <ReportModule :showTitle="false" :showDescribe="false" tableOrChart="table">
-    <template #title_left>
-      <el-input v-model="state.keyWord" style="width: 200px" placeholder="请输入学号或姓名" class="input_with">
-        <template #append>
-          <el-button :icon="Search" />
-        </template>
-      </el-input>
-      <span class="count_item">应考:293人</span>
-      <span class="count_item">实考:280人</span>
-      <span class="count_item orange">缺考:13人</span>
+  <ReportModule
+    :showTitle="true"
+    :titleList="[state.groupTitle]"
+    :showDescribe="true"
+    tableOrChart="chart"
+    :showPrintBtn="false"
+    :showExportBtn="false"
+  >
+    <template #title_right>
+      <EchartType
+        :chartTypeList="state.problemAnalysisData.chartTypeList"
+        :current="state.problemAnalysisData.chartType"
+        @ChangeEchartType="
+          (val) => ChangeEchartType(val, 'problemAnalysisData')
+        "
+      />
+    </template>
+    <template #module_table_chart>
+      <template v-if="state.problemAnalysisData.data.length > 0">
+        <BarLineCharts
+          v-if="state.problemAnalysisData.chartType == 'line_bar_chart'"
+          :legendList="state.problemAnalysisData.legendList"
+          :showBarLegendIndex="state.problemAnalysisData.showBarLegendIndex"
+          title="得分率"
+          :data="state.problemAnalysisData.data"
+          @HandleChartClick="HandleChartClick"
+          @ChangeChartOrder="
+            (sortType, legendData, barIndex) =>
+              ChangeChartOrder(sortType, legendData, barIndex, 1)
+          "
+        >
+        </BarLineCharts>
+        <BarsCharts
+          v-if="state.problemAnalysisData.chartType == 'vertical_bar'"
+          :key="state.chartKey"
+          :data="state.problemAnalysisData.data"
+          :legendList="state.problemAnalysisData.legendList"
+          :showSortSelectbox="true"
+          unit="%"
+          title="得分率"
+          :isClick="true"
+          @HandleChartClick="HandleChartClick"
+          @ChangeChartOrder="
+            (sortType, legendData, barIndex) =>
+              ChangeChartOrder(sortType, legendData, barIndex, 1)
+          "
+        >
+        </BarsCharts>
+        <RadarCharts
+          v-if="state.problemAnalysisData.chartType == 'radar_chart'"
+          :key="state.chartKey"
+          :data="state.problemAnalysisData.data"
+          :legendList="state.problemAnalysisData.legendList"
+          :showCheckBox="true"
+          :openShowAllLegend="true"
+          :isClick="true"
+          @HandleChartClick="HandleChartClick"
+        >
+        </RadarCharts>
+      </template>
+      <div
+        v-else
+        class="no_content_data"
+        v-loading="state.dataLoading"
+        :element-loading-text="state.loadingText"
+        element-loading-spinner="el-icon-loading"
+        element-loading-background="#ffffff"
+      >
+        <span>暂无数据</span>
+      </div>
     </template>
+    <template #module_describe>
+      展示每道试题的得分率图。得分率指实际得分/考核分的比值,换算成的百分数。可以用于分析每道试题的难易程度和质量,试题得分率高意味着试题难度低或者学生整体水平高;得分率低意味着试题难度较高或者考生整体水平较低。点击每道试题的柱或雷达图的题号可在下方查看该题每个班的得分率情况。
+    </template>
+  </ReportModule>
+  <ReportModule
+    :showTitle="true"
+    :titleList="[state.groupTitle, state.questionTitle]"
+    :showDescribe="true"
+    tableOrChart="chart"
+    :showPrintBtn="false"
+    :showExportBtn="false"
+  >
     <template #title_right>
-      <el-checkbox-group class="checkbox_group" v-model="state.checkList">
-        <el-checkbox label="显示分组" value="group" />
-        <el-checkbox label="显示小题" value="question" />
-      </el-checkbox-group>
+      <EchartType
+        :chartTypeList="state.questionScoreStatsData.chartTypeList"
+        :current="state.questionScoreStatsData.chartType"
+        @ChangeEchartType="
+          (val) => ChangeEchartType(val, 'questionScoreStatsData')
+        "
+      />
     </template>
     <template #module_table_chart>
-      <el-table :data="tableData" border style="width: 100%">
-        <el-table-column prop="date" label="Date" width="180" />
-        <el-table-column prop="name" label="Name" width="180" />
-        <el-table-column prop="address" label="Address" />
+      <template v-if="state.questionScoreStatsData.datax.length > 0">
+        <BarScoringRateVertical
+          v-if="state.questionScoreStatsData.chartType == 'vertical_bar'"
+          :datax="state.questionScoreStatsData.datax"
+          :datay="state.questionScoreStatsData.dataStackY"
+          :tooltipData="state.questionScoreStatsData.tooltipData"
+          :isShowMarkLine="true"
+          :average="state.questionScoreStatsData.rate"
+          :markLineData="state.questionScoreStatsData.markLineData"
+          :color="['#3BA272', '#EE6666']"
+          typeName="得分率"
+          :isClick="true"
+          @HandleChartClick="HandleQuestionScoreChartClick"
+        ></BarScoringRateVertical>
+        <DifferenceChart
+          v-if="state.questionScoreStatsData.chartType == 'difference_chart'"
+          :datax="state.questionScoreStatsData.datax"
+          :datay="state.questionScoreStatsData.datay"
+          unit="%"
+          type="1"
+          title="得分率"
+          :rate="state.questionScoreStatsData.rate"
+          @HandleChartClick="HandleQuestionScoreChartClick"
+        >
+        </DifferenceChart>
+      </template>
+      <div
+        v-else
+        class="no_content_data"
+        v-loading="state.dataLoading"
+        :element-loading-text="state.loadingText"
+        element-loading-spinner="el-icon-loading"
+        element-loading-background="#ffffff"
+      >
+        <span>暂无数据</span>
+      </div>
+    </template>
+    <template #module_describe>
+      说明:通过上面图形,可查看该题每个班的得分率情况,由图中可以看出,得分率最高为<span
+        style="color: #3ba272"
+        >{{ state.questionScoreStatsData.maxClass }}</span
+      >,最低为<span style="color: #ee6666">{{
+        state.questionScoreStatsData.minClass
+      }}</span
+      >。点击每个班级的柱可在下方查看该班各个选项/得分的人数分布。
+    </template>
+  </ReportModule>
+  <ReportModule
+    :showTitle="true"
+    :titleList="[state.groupTitle, state.questionTitle, state.classTitle]"
+    :showDescribe="true"
+    tableOrChart="qita"
+    :showPrintBtn="false"
+    :showExportBtn="false"
+  >
+    <template #module_qita>
+      <div class="content_left answer">
+        <BarChart
+          v-if="state.questionAnswerData.datax.length"
+          :datax="state.questionAnswerData.datax"
+          :datay="state.questionAnswerData.datay"
+          typeName="人数"
+          :showNuitY="false"
+          unit="人"
+          :unitX="
+            state.questionAnswerData.questionType == '单选题' ||
+            state.questionAnswerData.questionType == '多选题' ||
+            state.questionAnswerData.questionType == '判断题'
+              ? ''
+              : '分'
+          "
+          :showMarkPoint="false"
+          :isShowMarkLine="false"
+          :color="state.questionAnswerData.color"
+          :answerValue="state.questionAnswerData.answerValue"
+          :answerScore="state.questionAnswerData.answerScore"
+          :fullMark="state.questionAnswerData.fullMark"
+          :isClick="true"
+          @HandleChartClick="HandleQuestionAnswerChartClick"
+          style="height: 300px"
+        ></BarChart>
+        <div
+          v-else
+          class="module_chart no_content_data"
+          v-loading="state.dataLoading"
+          :element-loading-text="state.loadingText"
+          element-loading-spinner="el-icon-loading"
+          element-loading-background="#ffffff"
+        >
+          <span>暂无数据</span>
+        </div>
+      </div>
+      <div class="content_right answer table_42">
+        <el-table
+          border
+          :data="state.questionAnswerData.tableData"
+          stripe
+          align="left"
+          :header-row-style="HeaderRowStyle"
+        >
+          <el-table-column :label="state.classTitle" align="center">
+            <el-table-column
+              prop="title"
+              align="center"
+              label="名称"
+            ></el-table-column>
+            <el-table-column
+              prop="value"
+              align="center"
+              label="数值"
+            ></el-table-column>
+          </el-table-column>
+        </el-table>
+      </div>
+    </template>
+    <template #module_describe>
+      说明:通过柱图,可查看该题下该班所有学生的选项或试题得分情况。点击每个柱可查看该选项或分数的学生明细和答题卡答题情况。
+    </template>
+  </ReportModule>
+  <ReportModule
+    :showTitle="true"
+    :titleList="[
+      state.groupTitle,
+      state.questionTitle,
+      state.classTitle,
+      state.optionTitle,
+    ]"
+    :showDescribe="true"
+    tableOrChart="qita"
+    :showPrintBtn="false"
+    :showExportBtn="false"
+  >
+    <template #title_right>
+      <el-button class="default_button" @click="VisibleQuestionCard">
+        <img src="@/assets/icon/card_view.webp" />批量查看
+      </el-button>
+    </template>
+    <template #module_qita>
+      <div class="content_left card"></div>
+      <div class="content_right card table_42">
+        <el-table
+          border
+          :data="state.majorAnswerData.tableData"
+          stripe
+          highlight-current-row
+          align="left"
+          height="401px"
+          row-key="studentRegistrationCode"
+          :row-style="{ cursor: 'pointer' }"
+          :row-class-name="TableRowClassName"
+          @row-click="HandleRowClick"
+        >
+          <el-table-column
+            prop="studentUserName"
+            align="center"
+            label="学生"
+            show-overflow-tooltip
+          ></el-table-column>
+          <el-table-column
+            label="班级"
+            prop="className"
+            align="center"
+            min-width="70"
+            show-overflow-tooltip
+          ></el-table-column>
+          <el-table-column
+            prop="questionScore"
+            align="center"
+            label="得分"
+            width="70"
+            show-overflow-tooltip
+          ></el-table-column>
+          <el-table-column
+            prop="paperScore"
+            align="center"
+            label="成绩"
+            width="70"
+            show-overflow-tooltip
+          ></el-table-column>
+          <el-table-column align="center" label="操作">
+            <template #default="scope">
+              <el-button type="text" @click.stop="handleClick(scope.row)"
+                >查看答题卡</el-button
+              >
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </template>
+    <template #module_describe>
+      说明:点击左侧表格列可切换学生查看具体学生答题图片,点击学生姓名可在下方查看学生均衡分析,点击学生成绩可查看学生答题卡原卷。
+    </template>
+  </ReportModule>
+  <ReportModule
+    :showTitle="true"
+    :titleList="[state.groupTitle]"
+    :showDescribe="true"
+    tableOrChart="table"
+    :showPrintBtn="false"
+    :showExportBtn="true"
+    :currentPage="state.majorTableData.currentPage"
+    :pageSize="state.majorTableData.pageSize"
+    :total="state.majorTableData.total"
+    @update:pageSize="handleSizeChange"
+    @update:currentPage="handleCurrentChange"
+  >
+    <template #title_right></template>
+    <template #module_table_chart>
+      <el-table
+        :data="state.majorTableData.tableData"
+        ref="majorTable"
+        border
+        stripe
+        align="left"
+        :key="state.majorTableData.tableKey"
+      >
+        <template v-for="item in state.majorTableData.headerData">
+          <el-table-column
+            v-if="item.display"
+            :key="item.prop"
+            align="center"
+            :prop="item.prop"
+            :min-width="item.label.length > 4 ? 110 : 90"
+            :label="item.label"
+            fixed="left"
+            show-overflow-tooltip
+          >
+            <template #default="scope">
+              {{ scope.row[item.prop] || "-"
+              }}{{
+                item.prop == "estimatedScoreRate" && scope.row[item.prop]
+                  ? "%"
+                  : ""
+              }}
+            </template>
+          </el-table-column>
+        </template>
+        <template v-for="parent in state.majorTableData.changeHeaderData">
+          <el-table-column
+            v-if="parent.display"
+            align="center"
+            :prop="parent.prop"
+            :label="parent.label"
+            :key="parent.prop"
+          >
+            <template v-for="item in state.majorTableData.childHeaderData">
+              <el-table-column
+                v-if="item.display"
+                :prop="`${parent.prop}_${item.prop}`"
+                :key="`${parent.prop}_${item.prop}`"
+                align="center"
+                :label="item.label"
+                :min-width="item.label.length > 4 ? 100 : 80"
+                show-overflow-tooltip
+              >
+                <template #default="scope">
+                  <span v-if="item.prop.indexOf('Rate') > -1">{{
+                    scope.row[`${parent.prop}_${item.prop}`]
+                      ? `${scope.row[`${parent.prop}_${item.prop}`]}%`
+                      : "-"
+                  }}</span>
+                  <span
+                    v-else
+                    @click="OpenStudentDialog(item, parent, scope.row)"
+                    :class="{
+                      table_row_blue:
+                        (item.prop.indexOf('Count') > -1 ||
+                          item.prop.indexOf('Number') > -1) &&
+                        scope.row?.[`${parent.prop}_${item.prop}`] > 0,
+                    }"
+                    >{{
+                      scope.row?.[`${parent.prop}_${item.prop}`] ?? "-"
+                    }}</span
+                  >
+                </template>
+              </el-table-column>
+            </template>
+          </el-table-column>
+        </template>
       </el-table>
     </template>
   </ReportModule>
+  <!-- 批量查看小题答题卡 -->
+  <!-- <QuestionCard :subjectId="state.analysisStore.filterObject.subjectId" :questionId="state.cardQuestionId" :platformNumbers="cardRegistrationCodeList" :groupTitle="groupTitle" :groupName="groupName" :questionTitle="questionTitle" :classTitle="classTitle" :optionTitle="optionTitle" :tagActive="tagActive" :showDialog="showQuestionCardDialog" @CloseDialog="CloseQuestionCardDialog"></QuestionCard> -->
 </template>
 <script lang="ts" setup>
-import ReportModule from '@/components/ReportModule.vue';
-import { Search } from '@element-plus/icons-vue'
-import { onMounted, reactive, ref } from "vue";
-const tableData = [
-  {
-    date: '2016-05-03',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
+import ReportModule from "@/components/ReportModule.vue";
+import EchartType from "@/components/EchartType.vue";
+import { useAnalysisStore } from "@/store/analysis";
+import {
+  questionAnalysis,
+  queryAnswerListByAnswerAndScore,
+} from "@/api/analysis";
+import BarLineCharts from "@/components/echarts/barLineCharts.vue"; //柱状图折线图组合图组件
+import RadarCharts from "@/components/echarts/radarCharts.vue"; //雷达图
+import BarsCharts from "@/components/echarts/barsCharts.vue"; //多柱状图组件
+import BarScoringRateVertical from "@/components/echarts/barScoringRate_vertical.vue"; //得分率 纵向柱状图
+import DifferenceChart from "@/components/echarts/differenceChart.vue"; //率差图
+import BarChart from "@/components/echarts/barChart_answer.vue"; //单柱状图
+import { onMounted, reactive, watch, ref, nextTick } from "vue";
+import { cloneDeep } from "lodash-es";
+const analysisStore = useAnalysisStore();
+const state = reactive({
+  questionGroupDefault: [
+    {
+      name: "小题分析",
+      code: "problem",
+    },
+    {
+      name: "题型分析",
+      code: 11,
+    },
+    {
+      name: "错题分析",
+      code: "errors",
+    },
+    {
+      name: "客观题分析",
+      code: "selectQuestion",
+    },
+    {
+      name: "命题分析",
+      code: "proposition",
+    },
+  ],
+  questionGroupList: [], //试题分组标签 动态接口获取
+  tagActive: "problem", //选择的试题分组标签
+  groupTitle: "小题分析",
+  groupPreviousTitle: "",
+  knowledgeLayeredTitle: "", //知识点分层标题
+  groupName: "", // 分组名称
+  questionTitle: "", //题目名称
+  classTitle: "", //班级名称
+  optionTitle: "", //选项名称
+  studentUserName: "", //学生名称
+  studentRegistrationCode: "", //学生code
+  questionTypesData: {
+    chartKey: 0,
+    refresh: false,
+    data: [],
+    tableData: [],
   },
-  {
-    date: '2016-05-02',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
+  problemAnalysisData: {
+    chartType: "line_bar_chart", //默认显示折线图柱状图line_bar_chart
+    chartTypeList: [
+      {
+        label: "组合图",
+        value: "line_bar_chart",
+      },
+      {
+        label: "柱状图",
+        value: "vertical_bar",
+      },
+      {
+        label: "雷达图",
+        value: "radar_chart",
+      },
+    ],
+    legendList: [],
+    defaultLegendList: [],
+    showBarLegendIndex: 1,
+    questionList: [], // 题目列表
+    questionTableList: [], // 小题分析table
+    groupList: [], // 分组题目列表
+    groupTableList: [], // 分组table
+    headerList: [],
+    changeHeaderList: [],
+    childHeaderList: [],
+    questionListIndex: 0, //题目 索引
+    data: [], //柱状图
+  }, //小题分析数据
+  majorQuestionData: {
+    chartType: "line_bar_chart", //默认显示折线图柱状图line_bar_chart
+    legendList: [],
+    defaultLegendList: [],
+    showBarLegendIndex: 1,
+    questionListIndex: 0, //题目 索引
+    data: [], //柱状图
+  }, //分组题目 对应的题目列表
+  questionScoreStatsData: {
+    checked: false,
+    isIndeterminate: true,
+    markLineData: [], //平均分
+    chartTypeList: [
+      {
+        label: "柱状图",
+        value: "vertical_bar",
+      },
+      {
+        label: "率差图",
+        value: "difference_chart",
+      },
+    ],
+    chartType: "vertical_bar", //默认显示率差图vertical_bar
+    datax: [], //x轴数据
+    datay: [], //Y轴数据
+    dataStackY: [], //Y轴数据
+    tooltipData: [], //
+    rate: 60, //中间x的刻度线的值 数字类型
+    maxClass: "", // 得分率最高班
+    minClass: "", // 得分率最低班
+  }, //小题分析 /第N题
+  questionAnswerData: {
+    datax: [], //x轴数据
+    datay: [], //Y轴数据
+    color: "#5470C6",
+    answerValue: "", //正确答案 柱状图显示绿色
+    answerScore: [], //答案所能得的分数
+    questionType: "", //类型
+    fullMark: "", //满分
+    //   average:0,//平均分辅助线
+    tableData: [],
+    classListIndex: 0, // 索引
+  }, //小题分析 /第N题 / N班
+  majorAnswerData: {
+    paperUrl: "", //试卷地址
+    tableData: [], //表格数据
+    rowIndex: 0,
+  }, // 通过答案或者分数查询某题作答情况
+  majorStudentData: {
+    tableData: [],
+    legendList: [],
+    radarChartData: [],
+    excellentCourseList: "", //优秀科目
+    inferiorCourseList: "", //弱势科目
+  }, //科目得分率
+  majorTableData: {
+    tableKey: 0,
+    tableData: [], //小题分析表数据
+    allTableData: [],
+    headerData: [], //固定表头数据
+    changeHeaderData: [], //动态表头数据
+    childHeaderData: [], //子级表头数据
+    pageSize: 10, //每页显示数据
+    total: 0, //总数
+    currentPage: 1, //当前页
+  }, //小题分析表 + 题目分组分析
+  errorQuestionData: {
+    tableKey: 0,
+    allData: [], //
+    gradeValue: "", //选中的年级
+    className: "", //选中的年级名称
+    type: "",
+    gradeData: [], //年级下拉选项
+    tableData: [], //错题分析表数据
+  }, //错题分析表
+  studentPreviousExamData: {
+    data: null,
+    chartType: "line_chart",
+    selectList: [
+      {
+        value: "standardScore",
+        name: "标准分",
+      },
+      {
+        value: "scoreRate",
+        name: "得分率",
+      },
+      {
+        value: "classRank",
+        name: "班排",
+      },
+      {
+        value: "schoolRank",
+        name: "校排",
+      },
+      {
+        value: "examRank",
+        name: "联排",
+      },
+    ],
+    selectVal: "standardScore",
+    selectName: "标准分",
+    lineChartData: {
+      datax: [],
+      datay: [],
+      title: [],
+      tooltipData: [],
+    },
+    barChartData: {
+      //柱状图
+      legendList: [],
+      data: [],
+    },
+  }, //学生成绩历次考试分析数据
+  groupPrevious: {
+    loading: false,
+    examNameList: [],
+    examChartList: [],
+    selectedLegendList: [],
+    examChartIndex: [],
+    headerList: [],
+    dataList: [],
+  }, //题目分组历次分析(图表数据)
+  stuGroupPreviousChart: {
+    examNameList: [],
+    examChartList: [],
+    examChartIndex: [],
+  }, //学生组块历次图
+  stuGroupPreviousChartStuInfo: {
+    studentName: "", //学生姓名搜索条件
+    studentCodeList: [],
+  }, //学生历次分析图 搜索学生信息
+  stuGroupPrevious: {
+    total: 0,
+    pageNum: 1,
+    pageSize: 10,
+    headerList: [],
+    tableList: [],
+  }, //题目分组历次分析学生数据表(分页)
+  classNameList: [],
+  showHeaderSet: false, //是否显示设置表头
+  showBenchTaskSelect: false, //历次考试弹框
+  exportLoading: false, // 题型导出loading
+  exportErrorLoading: false, // 错题导出loading
+  chartKey: 0,
+
+  dataLoading: false,
+  loadingText: "加载中,请稍后……",
+
+  errorPdfLoading: false, //错题导出PDF loading
+  paperInfo: {
+    examPaperId: "", //考试科目id
+    platformNumber: "", //学籍号平台号
+    questionId: "", //题目id
+  }, //学生试卷信息
+  paperTitle: "", //学生试卷标题
+  showStudentPaperDialog: false, //是否显示学生答题卡弹窗
+  paperInfos: {
+    examPaperId: "", //考试科目id
+    platformNumber: "", //学籍号平台号
+    questionId: "", //题目id
   },
-  {
-    date: '2016-05-04',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
+  knowledgeInputDialog: false,
+  dialogData: {
+    apiName: "",
+    showDialog: false,
+    title: "",
+    tableTitle: "",
+    fiveRateName: "", //当前选择的五率的名称
+    selectSubjectName: "", //当前选择的科目名称
+    selectSchoolLevel: "", //当前选择的学校级别:0-联考 1-学校分组 2-具体学校
+    selectSchoolName: "", //当前选择的学校名称:联校 组合学校 单校名称
+    selectClassLevel: "", //当前选择的班级级别:0-年级 1-组合班级 2-具体班级
+    selectClassName: "", //当前选择的班级名称:年级 组合班级 单班
+    questionId: "", //试题ID
+    isPaper: "", //是否为全卷0-非1-是全卷
+    quesScoreType: "", //选择的类型0-满分人数 1-0分人数 3-优秀率、良好率等
+    knowledgeId: "", //如果选择的是知识点,则需要返回知识点ID
+    questionGroupName: "", //试题分组的名称
+    groupTitle: "",
   },
-  {
-    date: '2016-05-01',
-    name: 'Tom',
-    address: 'No. 189, Grove St, Los Angeles',
+  showQuestionCardDialog: false,
+  cardQuestionId: "", // 批量查看答题卡试题id
+  cardRegistrationCodeList: [], // 批量查看答题卡学生账号数组
+});
+const majorTable = ref(null);
+//排序
+const ChangeChartOrder = (sortType, legendData, barIndex, number) => {
+  const isHasEstimatedScore = state.problemAnalysisData.headerList.find(
+    (item) => item.prop == "estimatedScore",
+  ); //是否存在预估满分
+  const fullVolume = state.problemAnalysisData.questionList.filter(
+    (item) => item.showCode == 999,
+  ); //全卷
+  const questionList = state.problemAnalysisData.questionList.filter(
+    (item) => item.showCode != 999,
+  );
+  const newBarIndex = isHasEstimatedScore ? barIndex - 1 : barIndex;
+  if (sortType == "2") {
+    //从低到高
+    questionList.sort(function (a, b) {
+      return (
+        (a?.classList?.[newBarIndex]?.questionStats?.scoreRate || 0) -
+        (b?.classList?.[newBarIndex]?.questionStats?.scoreRate || 0)
+      );
+    });
+  } else if (sortType == "3") {
+    //从高到低
+    questionList.sort(function (a, b) {
+      return (
+        (b?.classList?.[newBarIndex]?.questionStats?.scoreRate || 0) -
+        (a?.classList?.[newBarIndex]?.questionStats?.scoreRate || 0)
+      );
+    });
+  } else {
+    //默认排序 按题号排序
+    questionList.sort(function (a, b) {
+      return (a?.showCode || 0) - (b.showCode || 0);
+    });
+  }
+  state.problemAnalysisData.questionList = [...questionList, ...fullVolume];
+  let chartData = [],
+    chartTitle = [];
+  state.problemAnalysisData.questionList.forEach((ques) => {
+    const scoreRateArr = ques.classList.map(
+      (item) => item?.questionStats?.scoreRate || 0,
+    );
+    if (isHasEstimatedScore) {
+      chartData.push([
+        ques.questionName,
+        ques.estimatedScoreRate,
+        ...scoreRateArr,
+      ]);
+    } else {
+      chartData.push([ques.questionName, ...scoreRateArr]);
+    }
+  });
+  const newChangeHeaderList = isHasEstimatedScore
+    ? [
+        { ...isHasEstimatedScore, isG: false, type: 1 },
+        ...state.problemAnalysisData.changeHeaderList,
+      ]
+    : [...state.problemAnalysisData.changeHeaderList];
+  newChangeHeaderList.forEach((item) => {
+    chartTitle.push(item.label);
+  });
+  chartTitle.unshift("group");
+  chartData.unshift(chartTitle); // 柱状图 图例标题
+  chartData.pop(); // 删除最后一行 全卷
+  state.problemAnalysisData.data = chartData;
+  state.problemAnalysisData.legendList = legendData;
+  //小题分析 /第N题
+  GetQuestionStatsData(0);
+};
+//试题图表切换公共方法
+const ChangeEchartType = (value, prop) => {
+  if (prop == "problemAnalysisData") {
+    ChangeChartOrder("1", state.problemAnalysisData.defaultLegendList, "", 1);
+  }
+  state[prop].chartType = value;
+};
+//获取小题分析数据
+const GetQuestionAnalysisData = () => {
+  state.problemAnalysisData.data = []; //柱状图折线图 X轴
+  state.dataLoading = true;
+  questionAnalysis({
+    ...analysisStore.filterObject,
+    analysisType: 0, //0-小题分析 1大题分析 2-知识点 3-能力点 question_group_code(4,5,6,7,8) 11-题型分析
+  })
+    .then((res) => {
+      if (
+        res.code == 200 &&
+        res.data &&
+        res.data.questionList &&
+        res.data.questionList.length
+      ) {
+        const { data } = res;
+        const questionList = data.questionList || [];
+        const changeHeaderList = data.changeHeaderList || []; //柱状图折线图 班级名称
+        state.problemAnalysisData.groupList = [];
+        state.problemAnalysisData.groupTableList = [];
+        state.problemAnalysisData.questionList = questionList;
+        state.problemAnalysisData.questionTableList = cloneDeep(questionList);
+        state.problemAnalysisData.headerList = data.headerList || [];
+        state.problemAnalysisData.changeHeaderList = changeHeaderList;
+        state.problemAnalysisData.childHeaderList = data.childHeaderList || [];
+
+        state.chartKey++;
+        //Y轴数据
+        let chartData = [];
+        let legendList = [];
+        const isHasEstimatedScore = state.problemAnalysisData.headerList.find(
+          (item) => item.prop == "estimatedScore",
+        ); //是否存在预估满分
+        chartData = questionList.map((item) => {
+          return isHasEstimatedScore
+            ? [item.questionName, item.estimatedScoreRate]
+            : [item.questionName];
+        });
+        questionList.forEach((ques, key) => {
+          const scoreRateArr = ques.classList.map(
+            (item) => item?.questionStats?.scoreRate || 0,
+          );
+          chartData[key].push(...scoreRateArr);
+        });
+        let chartTitle = [],
+          titleType = [],
+          classSelectLegend = [];
+        const newChangeHeaderList = isHasEstimatedScore
+          ? [
+              { ...isHasEstimatedScore, isG: false, type: 1 },
+              ...changeHeaderList,
+            ]
+          : [...changeHeaderList];
+        newChangeHeaderList.forEach((item) => {
+          chartTitle.push(item.label);
+          titleType.push(item.type ? item.type : ""); //1柱状图 2折线
+          if (
+            item.prop != "0" &&
+            item.prop != "estimatedScore" &&
+            item.prop.indexOf("school_group") == -1
+          ) {
+            classSelectLegend.push(item.label);
+          }
+        });
+        chartTitle.unshift("group");
+        if (analysisStore.filterObject.classLevel != 2) {
+          //0 年级 1 组合班级 2具体班级
+          let startIndex = 1,
+            endIndex = 3;
+          if (analysisStore.filterObject.schoolLevel == 2) {
+            //0-联考 1-学校分组 2-具体学校
+            startIndex =
+              titleType.lastIndexOf(1) > -1 ? titleType.lastIndexOf(1) + 1 : 1;
+            endIndex = titleType.indexOf(2) > -1 ? titleType.indexOf(2) + 2 : 3; //单校时显示联校、组合校、年级是柱子;班级组合、班级是折线。
+          } else {
+            //联校 组合学校 默认当前的为第一个选中的柱子
+            const barFirtIndex =
+              titleType.lastIndexOf(1) > -1 ? titleType.lastIndexOf(1) + 1 : 1;
+            startIndex = barFirtIndex;
+            endIndex = barFirtIndex + 2;
+          }
+          legendList =
+            chartTitle.length > 1 ? chartTitle.slice(startIndex, endIndex) : [];
+          state.problemAnalysisData.showBarLegendIndex = endIndex - 2;
+        } else {
+          //单班 联校和组合学校不默认显示
+          legendList = classSelectLegend;
+          state.problemAnalysisData.showBarLegendIndex =
+            changeHeaderList.length - 1;
+        }
+        chartData.unshift(chartTitle); // 柱状图 图例标题
+        chartData.pop(); // 删除最后一行 全卷
+        state.problemAnalysisData.data = chartData;
+        state.problemAnalysisData.legendList = legendList;
+        state.problemAnalysisData.defaultLegendList = cloneDeep(legendList);
+        //小题分析 /第N题
+        GetQuestionStatsData(0);
+        // 获取项目分析表
+        GetMajorTableData();
+      } else {
+        state.problemAnalysisData.groupList = [];
+        state.problemAnalysisData.groupTableList = [];
+        state.problemAnalysisData.questionList = [];
+        state.problemAnalysisData.questionTableList = [];
+        state.problemAnalysisData.showBarLegendIndex = 1;
+        state.problemAnalysisData.headerList = [];
+        state.problemAnalysisData.changeHeaderList = [];
+        state.problemAnalysisData.childHeaderList = [];
+
+        state.majorQuestionData.data = [];
+        state.majorQuestionData.legendList = [];
+
+        state.questionScoreStatsData.datax = [];
+        state.questionScoreStatsData.datay = [];
+        state.questionScoreStatsData.dataStackY = [];
+        state.questionScoreStatsData.tooltipData = [];
+        state.questionScoreStatsData.rate = 0;
+        state.questionScoreStatsData.maxClass = "";
+        state.questionScoreStatsData.minClass = "";
+
+        state.questionAnswerData.datax = [];
+        state.questionAnswerData.datay = [];
+        state.questionAnswerData.answerValue = "";
+        state.questionAnswerData.answerScore = [];
+        state.questionAnswerData.questionType = "";
+        state.questionAnswerData.fullMark = "";
+        state.questionAnswerData.tableData = [];
+        state.questionAnswerData.classListIndex = 0;
+
+        state.majorAnswerData.tableData = [];
+        state.majorAnswerData.paperUrl = "";
+        state.majorAnswerData.rowIndex = 0;
+
+        state.majorStudentData.tableData = [];
+        state.majorStudentData.legendList = [];
+        state.majorStudentData.radarChartData = [];
+        state.majorStudentData.excellentCourseList = "";
+        state.majorStudentData.inferiorCourseList = "";
+
+        state.majorTableData.tableData = [];
+        state.majorTableData.headerData = [];
+        state.majorTableData.changeHeaderData = [];
+        state.majorTableData.childHeaderData = [];
+        state.majorTableData.pageSize = 10;
+        state.majorTableData.total = 0;
+        state.majorTableData.currentPage = 1;
+      }
+    })
+    .finally(() => {
+      state.dataLoading = false;
+    });
+};
+// 点击柱状图折线图
+const HandleChartClick = (index, name) => {
+  GetQuestionStatsData(index); //小题分析
+};
+// 获取小题分析 /第N题
+const GetQuestionStatsData = (index) => {
+  const questionData = cloneDeep(state.problemAnalysisData.questionList[index]);
+  state.problemAnalysisData.questionListIndex = index;
+  state.questionTitle = questionData?.questionName || "";
+  if (questionData) {
+    state.questionScoreStatsData.datax = []; // x轴数据
+    state.questionScoreStatsData.datay = []; // Y轴数据
+    state.questionScoreStatsData.dataStackY = []; // Y轴数据
+    state.questionScoreStatsData.tooltipData = []; // 提示框内容
+    let dataStackY = [];
+    const isHasEstimatedScore = state.problemAnalysisData.headerList.find(
+      (item) => item.prop == "estimatedScore",
+    ); //是否存在预估满分
+    if (questionData.classList.length > 1) {
+      const key = state.problemAnalysisData.showBarLegendIndex;
+      state.questionScoreStatsData.markLineData = []; //辅助线
+      const classList = questionData?.classList || [];
+      if (
+        analysisStore.filterObject.classLevel == 0 ||
+        analysisStore.filterObject.classLevel == 1
+      ) {
+        //0 年级 1 组合班级 2具体班级
+        state.questionScoreStatsData.rate = Number(
+          classList?.[key - 1]?.questionStats?.scoreRate || 0,
+        );
+      } else {
+        state.questionScoreStatsData.rate = Number(
+          classList?.[key]?.questionStats?.scoreRate || 0,
+        );
+      }
+      //辅助线
+      classList.forEach((item, index) => {
+        const keyIndex =
+          analysisStore.filterObject.classLevel == 0 ||
+          analysisStore.filterObject.classLevel == 1
+            ? key - (isHasEstimatedScore ? 2 : 1)
+            : key;
+        if (index <= keyIndex) {
+          const rate = Number(item?.questionStats?.scoreRate || 0);
+          state.questionScoreStatsData.markLineData.push({
+            legendName: item.groupName,
+            value: rate,
+            isShow: index == keyIndex ? true : false, //是否显示
+          });
+        }
+      });
+      questionData.classList = questionData.classList.slice(
+        isHasEstimatedScore ? key - 1 : key,
+      ); // 取折线图数据
+    } else {
+      state.questionScoreStatsData.rate = 60;
+    }
+
+    questionData.classList.forEach((item) => {
+      state.questionScoreStatsData.datax.push(item.groupName);
+      state.questionScoreStatsData.datay.push(item.questionStats.scoreRate);
+      dataStackY.push(item.questionStats.lossRate);
+      state.questionScoreStatsData.tooltipData.push({
+        list: [
+          {
+            name: "失分率",
+            value: `${item.questionStats.lossRate}%`,
+          },
+        ],
+      });
+    });
+    // 获取最大值
+    const max = Math.max(...state.questionScoreStatsData.datay);
+    // 获取最小值
+    const min = Math.min(...state.questionScoreStatsData.datay);
+    const maxValue = max.toFixed(2);
+    const minValue = min.toFixed(2);
+
+    let maxClass = [],
+      minClass = [];
+    state.questionScoreStatsData.datay.forEach((item, index) => {
+      if (Number(item) == Number(maxValue)) {
+        maxClass.push(state.questionScoreStatsData.datax[index]);
+      }
+      if (Number(item) == Number(minValue)) {
+        minClass.push(state.questionScoreStatsData.datax[index]);
+      }
+    });
+
+    state.questionScoreStatsData.minClass = minClass.join("、");
+    state.questionScoreStatsData.maxClass = maxClass.join("、");
+    state.questionScoreStatsData.dataStackY = [
+      state.questionScoreStatsData.datay,
+      dataStackY,
+    ];
+  }
+  console.log("打印questionScoreStatsData1111", state.questionScoreStatsData);
+  // 获取小题分析 /第N题 / 第N班
+  GetQuestionAnswerData(state.problemAnalysisData.showBarLegendIndex);
+};
+//切换 获取小题分析 /第N题 获取第N班答题列表
+const HandleQuestionScoreChartClick = (index) => {
+  GetQuestionAnswerData(index + state.problemAnalysisData.showBarLegendIndex);
+};
+// 获取小题分析 /第N题 / 第N班 选项
+const GetQuestionAnswerData = (index) => {
+  const isHasEstimatedScore = state.problemAnalysisData.headerList.find(
+    (item) => item.prop == "estimatedScore",
+  ); //是否存在预估满分
+  const newINdex = isHasEstimatedScore ? index - 1 : index;
+  const classList =
+    state.problemAnalysisData.questionList?.[
+      state.problemAnalysisData.questionListIndex
+    ]?.classList?.[newINdex];
+  // console.log("打印获取小题分析班级列表",classList)
+  state.questionAnswerData.answerValue =
+    state.problemAnalysisData.questionList[
+      state.problemAnalysisData.questionListIndex
+    ].answerValue; //正确答案
+
+  state.questionAnswerData.questionType =
+    state.problemAnalysisData.questionList[
+      state.problemAnalysisData.questionListIndex
+    ].questionType; //类型
+  state.questionAnswerData.fullMark =
+    state.problemAnalysisData.questionList[
+      state.problemAnalysisData.questionListIndex
+    ].fullMark; //满分
+
+  state.questionAnswerData.classListIndex = newINdex;
+  if (classList) {
+    const answerList = classList.questionStats.answerList || [];
+    state.classTitle = classList.groupName;
+    state.questionAnswerData.datax = []; // x轴数据
+    state.questionAnswerData.datay = []; // Y轴数据
+    let sum = 0;
+    answerList.forEach((item) => {
+      sum += item.studentNum;
+      state.questionAnswerData.datax.push(item.name);
+      state.questionAnswerData.datay.push(item.studentNum);
+      state.questionAnswerData.answerScore.push(item.score);
+    });
+    state.questionAnswerData.tableData = [
+      {
+        title: "得分率",
+        value: `${classList.questionStats.scoreRate}%`,
+      },
+      {
+        title: "平均分",
+        value: `${classList.questionStats.averageScore}`,
+      },
+      {
+        title: "最高分",
+        value: `${classList.questionStats.maxScore}`,
+      },
+      {
+        title: "最低分",
+        value: `${classList.questionStats.minScore}`,
+      },
+      {
+        title: "人数",
+        value: `${sum}人`,
+      },
+    ];
+    if (analysisStore.filterObject.schoolLevel == 2) {
+      //单校时展示
+      // 获取答题情况
+      GetAnswerListByAnswerAndScore(
+        0,
+        answerList && answerList[0] ? answerList[0].name : "",
+      );
+    }
+  }
+};
+// 点击柱状图小题分析 /第N题 / 第N班 选项 获取答题情况
+const HandleQuestionAnswerChartClick = (index, name) => {
+  if (analysisStore.filterObject.schoolLevel == 2) {
+    //单校时展示
+    GetAnswerListByAnswerAndScore(index, name);
+  }
+};
+//通过答案或者分数查询某题作答情况
+const GetAnswerListByAnswerAndScore = (index, name) => {
+  state.optionTitle = name; //选项名称
+  const question =
+    state.problemAnalysisData.questionList[
+      state.problemAnalysisData.questionListIndex
+    ];
+  const classItem = question.classList[state.questionAnswerData.classListIndex];
+  const classItemKeys = Object.keys(classItem);
+  const reportParam = {
+    ...analysisStore.filterObject,
+  };
+  Object.keys(analysisStore.filterObject).forEach((item) => {
+    if (classItemKeys.indexOf(item) > -1) {
+      reportParam[item] = classItem[item];
+    }
+  });
+  const answer =
+    question.classList[state.questionAnswerData.classListIndex].questionStats
+      .answerList[index];
+  const params = {
+    ...reportParam,
+    questionId: question.questionId, // 试题id
+    registrationCodeList: answer?.registrationCodeList || [], // 学生账号数组
+  };
+  state.cardQuestionId = params.questionId; // 批量查看答题卡试题id
+  state.cardRegistrationCodeList = params.registrationCodeList; // 批量查看答题卡学生账号数组
+  queryAnswerListByAnswerAndScore(params).then((res) => {
+    if (res.code == 200) {
+      state.majorAnswerData.tableData = res.data || [];
+    } else {
+      state.majorAnswerData.tableData = [];
+    }
+  });
+};
+// 获取项目分析表
+const GetMajorTableData = () => {
+  state.majorTableData.tableKey += 1;
+  state.majorTableData.headerData = state.problemAnalysisData.headerList;
+  state.majorTableData.changeHeaderData =
+    state.problemAnalysisData.changeHeaderList;
+  state.majorTableData.childHeaderData =
+    state.problemAnalysisData.childHeaderList;
+  const headerPropData = state.problemAnalysisData.headerList.map(
+    (item) => item.prop,
+  ); //表头字段名
+  const childHeaderPropData = state.problemAnalysisData.childHeaderList.map(
+    (item) => item.prop,
+  ); //动态表头字段名
+  let allTableData = [];
+  const allList = state.problemAnalysisData.questionTableList;
+  allList.forEach((item) => {
+    let itemObj = {
+      questionId: item?.questionId || "",
+      knowledgeId: item?.knowledgeId || "",
+    };
+    const classList = item.classList;
+    headerPropData.forEach((title) => {
+      itemObj[title] = Array.isArray(item[title])
+        ? item[title].join("、")
+        : item[title];
+    });
+
+    classList.forEach((el) => {
+      if (
+        el.questionStats?.headDataBOList &&
+        el.questionStats.headDataBOList.length > 0
+      ) {
+        el.questionStats.headDataBOList.forEach((bo) => {
+          el.questionStats[`${bo.name}Rate`] = bo.rate;
+          el.questionStats[`${bo.name}StudentNumber`] = bo.studentNumber;
+        });
+      } else {
+        el.questionStats = [];
+      }
+      childHeaderPropData.forEach((field) => {
+        itemObj[`${el.groupId}_${field}`] = el.questionStats[field];
+      });
+    });
+    allTableData.push(itemObj);
+  });
+  state.majorTableData.total = allTableData.length; //总条数
+  state.majorTableData.allTableData = allTableData;
+  GetPageMajorTableData();
+  //重置表格滚动条位置
+  ResetTableScroll(); //重置表格滚动条位置
+};
+const GetPageMajorTableData = () => {
+  const start =
+    (state.majorTableData.currentPage - 1) * state.majorTableData.pageSize;
+  const end = start + state.majorTableData.pageSize;
+  state.majorTableData.tableData = state.majorTableData.allTableData.slice(
+    start,
+    end,
+  );
+};
+//重置表格滚动条位置
+const ResetTableScroll = () => {
+  nextTick(() => {
+    if (majorTable.value) {
+      const tableBody = majorTable.value.querySelector(
+        ".el-table__body-wrapper",
+      );
+      if (tableBody) {
+        tableBody.scrollTop = 0; //清除纵向滚动条位置
+        tableBody.scrollLeft = 0; // 清除横向滚动条位置
+      }
+    }
+  });
+};
+const handleCurrentChange = (val) => {
+  state.majorTableData.currentPage = val;
+  GetMajorTableData(); //加载分析表格数据
+};
+const handleSizeChange = (val: number) => {
+  state.majorTableData.pageSize = val;
+  state.majorTableData.currentPage = 1;
+  GetMajorTableData(); //加载分析表格数据
+}
+//设置表头样式
+const HeaderRowStyle = ({ row, rowIndex }) => {
+  if (rowIndex === 1) {
+    return {
+      display: "none",
+    };
+  }
+};
+const pageInit = () => {
+  state.problemAnalysisData.chartTypeList =
+    analysisStore.filterObject.classLevel != 2
+      ? [
+          {
+            label: "组合图",
+            value: "line_bar_chart",
+          },
+          {
+            label: "柱状图",
+            value: "vertical_bar",
+          },
+          {
+            label: "雷达图",
+            value: "radar_chart",
+          },
+        ]
+      : [
+          {
+            label: "柱状图",
+            value: "vertical_bar",
+          },
+          {
+            label: "雷达图",
+            value: "radar_chart",
+          },
+        ];
+  state.problemAnalysisData.chartType =
+    analysisStore.filterObject.classLevel != 2
+      ? "line_bar_chart"
+      : "vertical_bar";
+  state.questionScoreStatsData.chartType = "vertical_bar";
+  state.majorTableData.currentPage = 1;
+  state.majorAnswerData.rowIndex = 0;
+  state.studentPreviousExamData.selectVal = "standardScore";
+  state.studentPreviousExamData.selectName = "标准分";
+  GetQuestionAnalysisData(); //获取小题分析数据
+};
+// 监听筛选条件
+watch(
+  () => analysisStore.filterObject,
+  async () => {
+    pageInit();
   },
-]
-const state = reactive({
-  keyWord: '',
-  checkList: ['group']
+  { deep: true },
+);
+
+onMounted(() => {
+  pageInit();
 });
-onMounted(() => { });
 </script>
 
 <style lang="scss" scoped>
-.input_with {
-  margin-right: 10px;
+.content_left {
+  &.answer {
+    width: calc(100% - 220px);
+  }
+
+  &.card {
+    width: calc(100% - 480px);
+  }
 }
 
-.count_item {
-  font-weight: 400;
-  font-size: 16px;
-  color: #333333;
-  line-height: 24px;
-  margin-left: 10px;
+.content_right {
+  height: 100%;
+  display: flex;
+  align-items: center;
+  margin: auto;
+  padding-bottom: 0;
 
-  &.orange {
-    color: #FB9F34;
+  &.answer {
+    width: 200px;
+  }
+
+  &.card {
+    width: 460px;
   }
-}
 
-.checkbox_group {
-  :deep(.el-checkbox) {
-    margin-right: 10px;
+  :deep(.el-table) {
+    border-left: 0px;
+    border-right: 0px;
 
-    &:nth-child(1) {
-      margin-right: 20px;
+    .el-table__cell {
+      height: 44px;
     }
   }
 }