Pārlūkot izejas kodu

关联分析:科目成绩占初试总分权重对比分析

Michael Wang 3 gadi atpakaļ
vecāks
revīzija
908cfcdf73

+ 7 - 0
components.d.ts

@@ -5,20 +5,27 @@
 declare module 'vue' {
   export interface GlobalComponents {
     AButton: typeof import('ant-design-vue/es')['Button']
+    ACollapse: typeof import('ant-design-vue/es')['Collapse']
+    ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
     AForm: typeof import('ant-design-vue/es')['Form']
     AFormItem: typeof import('ant-design-vue/es')['FormItem']
     AInput: typeof import('ant-design-vue/es')['Input']
+    AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
     AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
     AMenu: typeof import('ant-design-vue/es')['Menu']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
     AModal: typeof import('ant-design-vue/es')['Modal']
     APopover: typeof import('ant-design-vue/es')['Popover']
+    ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
+    ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
     ASelect: typeof import('ant-design-vue/es')['Select']
     ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
     ASpin: typeof import('ant-design-vue/es')['Spin']
     ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
     ATable: typeof import('ant-design-vue/es')['Table']
     ATextarea: typeof import('ant-design-vue/es')['Textarea']
+    ATimeline: typeof import('ant-design-vue/es')['Timeline']
+    ATimelineItem: typeof import('ant-design-vue/es')['TimelineItem']
     CommonRangeConfig: typeof import('./src/components/CommonRangeConfig.vue')['default']
     CourseSelect: typeof import('./src/components/CourseSelect.vue')['default']
     CourseTypeSelect: typeof import('./src/components/CourseTypeSelect.vue')['default']

+ 13 - 1
src/api/projectCompareDetailPage.ts

@@ -1,5 +1,5 @@
 import { httpApp } from "@/plugins/axiosApp";
-import { SASPaper } from "@/types";
+import { SasCourse, SASPaper } from "@/types";
 
 /** 试卷特征量数对比分析 */
 export function getPaperCompareList(params: {
@@ -12,3 +12,15 @@ export function getPaperCompareList(params: {
     params
   );
 }
+
+/** 试卷特征量数对比分析 */
+export function getPaperCompareScoreList(params: {
+  contrastProjectId: number;
+  projectIds: number[];
+  courseCode: string;
+}) {
+  return httpApp.post<any, { data: SasCourse[] }>(
+    "/api/ess/contrast/course/list",
+    params
+  );
+}

+ 1 - 1
src/features/allAnalysis/ScoreRate.vue

@@ -68,7 +68,7 @@
                 <th>累计频数(%)</th>
               </tr>
               <tr v-for="(seg, index3) in course.rangeSegements" :key="index3">
-                <td>{{ seg[0] }}-</td>
+                <td>{{ seg[0] }}</td>
                 <td>{{ seg[1] }}</td>
                 <td v-number-to-percent>{{ seg[2] }}%</td>
                 <td>{{ seg[3] }}</td>

+ 37 - 8
src/features/projectCompareDetail/ProjectCompareDetail2.vue

@@ -33,6 +33,14 @@
           >说明</a-button
         > -->
       </div>
+      <div class="tw-flex tw-flex-1 tw-gap-4 tw-pt-4">
+        <ScoreRateCompare
+          v-if="typeof contrastProjectId === 'number'"
+          :projectIds="projectIds"
+          :contrastProjectId="contrastProjectId"
+          :courseCode="courseCode"
+        />
+      </div>
     </div>
   </div>
 </template>
@@ -45,6 +53,7 @@ import { onMounted, computed } from "vue";
 import { goBack } from "@/utils/utils";
 import { getPaperCompareList } from "@/api/projectCompareDetailPage";
 import type { EChartsOption } from "echarts";
+import ScoreRateCompare from "./ScoreRateCompare.vue";
 
 const store = useMainStore();
 store.currentLocation = "项目管理 / 关联分析";
@@ -54,7 +63,7 @@ let rootOrgId = $ref(undefined as unknown as number);
 let projectIds: number[] = $ref([]);
 let contrastProjectId: number = $ref();
 
-let data = $ref<SASPaper[]>([]);
+let attrCompareData = $ref<SASPaper[]>([]);
 
 async function fetchData() {
   if (projectIds.length === 0) {
@@ -71,7 +80,7 @@ async function fetchData() {
     courseCode,
   });
   // console.log(res);
-  data = res.data.map((v) => {
+  attrCompareData = res.data.map((v) => {
     v.avgScore = Math.round(v.avgScore * 100) / 100;
     v.stdev = Math.round(v.stdev * 100) / 100;
     v.coefficient = Math.round(v.coefficient * 100) / 100;
@@ -84,7 +93,7 @@ async function fetchData() {
 
 const segementsLine = computed(() => {
   // console.log(data, data.length);
-  if (data.length === 0) return;
+  if (attrCompareData.length === 0) return;
   return {
     // color: ["#003366", "#006699"],
     tooltip: {
@@ -96,7 +105,7 @@ const segementsLine = computed(() => {
     // legend: { data: [{ name: "xx" }, { name: "aa" }] },
     // legend: {},
     title: {
-      text: data[0].courseName + "试卷特征量数对比分析",
+      text: attrCompareData[0].courseName + "试卷特征量数对比分析",
       left: "50%",
       // right: "center",
       // right: "50%",
@@ -140,16 +149,36 @@ const segementsLine = computed(() => {
     series: [
       {
         type: "bar",
-        stack: "x",
+        stack: "a",
         // eslint-disable-next-line @typescript-eslint/no-unsafe-return
-        data: columns.map((v) => data[0][v.dataIndex]),
+        data: columns.map((v) => attrCompareData[0][v.dataIndex]),
       },
       {
         type: "bar",
-        stack: "y",
+        stack: "b",
         // eslint-disable-next-line @typescript-eslint/no-unsafe-return
-        data: columns.map((v) => data[1][v.dataIndex]),
+        data: columns.map((v) => attrCompareData[1][v.dataIndex]),
       },
+      ...(attrCompareData.length >= 3
+        ? [
+            {
+              type: "bar",
+              stack: "c",
+              // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+              data: columns.map((v) => attrCompareData[2][v.dataIndex]),
+            },
+          ]
+        : []),
+      ...(attrCompareData.length >= 4
+        ? [
+            {
+              type: "bar",
+              stack: "c",
+              // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+              data: columns.map((v) => attrCompareData[3][v.dataIndex]),
+            },
+          ]
+        : []),
       // { type: "bar", stack: "y", data: [6, 7, 8, 9, 10] },
     ],
   } as EChartsOption;

+ 375 - 0
src/features/projectCompareDetail/ScoreRateCompare.vue

@@ -0,0 +1,375 @@
+<template>
+  <div class="tw-flex tw-flex-1 tw-gap-4 tw-pt-4">
+    <div class="tw-bg-white tw-rounded-xl">
+      <div class="tw-mt-4"></div>
+      分数间隔
+      <a-select v-model:value="scoreGap">
+        <a-select-option :value="1">1</a-select-option>
+        <a-select-option :value="5">5</a-select-option>
+        <a-select-option :value="10">10</a-select-option>
+        <a-select-option :value="20">20</a-select-option>
+        <a-select-option :value="50">50</a-select-option>
+      </a-select>
+      <div class="tw-mt-4"></div>
+      <!-- <div style="flex-grow: 1">
+      <v-chart
+                class="chart"
+                :option="rangeSegementsLine(course)"
+                :autoresize="true"
+              />
+            </div> -->
+
+      <div>
+        <table v-if="hasFirstTryComparison" class="custom-table">
+          <tr>
+            <th rowspan="2">初试总分分数段</th>
+            <template v-for="i in courses.length" :key="i">
+              <th colspan="5" class="tw-text-center">
+                {{ courses[i - 1].projectName }}
+              </th>
+            </template>
+          </tr>
+          <tr>
+            <template v-for="i in courses.length" :key="i">
+              <th>人数占比(%)</th>
+              <th>初试总分平均分</th>
+              <th>本科目成绩平均分</th>
+              <th>本科目难度</th>
+              <th>占总分权重(%)</th>
+            </template>
+          </tr>
+          <tr v-for="(item, index) in courses[0].totalScoreRange" :key="index">
+            <!-- <tr  v-for="(course, i) in courses" :key="i" > -->
+            <td>
+              {{
+                scoreTitle(courses[0].totalScoreRangeTitle[index]) || "全体考生"
+              }}
+            </td>
+            <template v-for="(course, i) in courses" :key="i">
+              <td v-number-to-percent>
+                {{ course.totalScoreRange[index].countRate }}
+              </td>
+              <td v-round-number>
+                {{ course.totalScoreRange[index].avgTotalScore }}
+              </td>
+              <td v-round-number>
+                {{ course.totalScoreRange[index].avgScore }}
+              </td>
+              <td v-round-number>
+                {{ course.totalScoreRange[index].difficulty }}
+              </td>
+              <td v-number-to-percent>
+                {{ course.totalScoreRange[index].scoreRate }}
+              </td>
+            </template>
+          </tr>
+        </table>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { RangeConfig, SasCourse } from "@/types";
+import { message } from "ant-design-vue";
+import { onMounted, watch } from "vue";
+import { getPaperCompareScoreList } from "@/api/projectCompareDetailPage";
+import type { EChartsOption } from "echarts";
+import { RANGE_POINT_TYPE } from "@/constants/constants";
+
+// eslint-disable-next-line vue/no-setup-props-destructure
+const { projectIds, contrastProjectId, courseCode } = defineProps<{
+  projectIds: number[];
+  contrastProjectId: number;
+  courseCode: string;
+}>();
+
+// let courseId = $ref(undefined as undefined | number);
+
+let courses = $ref<SasCourse[]>([]);
+
+async function fetchData() {
+  if (projectIds.length === 0) {
+    void message.warn("请选择对比项目");
+    return;
+  }
+  if (typeof contrastProjectId !== "number") {
+    void message.warn("请选择参照项目");
+    return;
+  }
+  const res = await getPaperCompareScoreList({
+    projectIds,
+    contrastProjectId,
+    courseCode,
+  });
+  console.log(res);
+  res.data = res.data.map((v) => {
+    v.scoreRange = Object.values(
+      // eslint-disable-next-line
+      JSON.parse(<string>(<unknown>v.scoreRange) || "{}")
+    );
+    return v;
+  });
+  res.data = res.data.map((v) => {
+    v.rangeConfig = JSON.parse(<string>(<unknown>v.rangeConfig) || "0") || [
+      {
+        type: "ZERO",
+        baseScore: 0,
+        adjustScore: 0,
+      },
+    ];
+    v.totalScoreRange = JSON.parse(
+      <string>(<unknown>v.totalScoreRange) || "{}"
+    );
+    v.totalScoreRangeTitle = JSON.parse(
+      <string>(<unknown>v.totalScoreRangeTitle) || "{}"
+    );
+    return v;
+  });
+  res.data = res.data.map((v) => {
+    let acc = 0;
+    if (Array.isArray(v.scoreRange))
+      v.scoreRangeAcc = v.scoreRange.map((_v, i, a) => {
+        acc += a[i];
+        return acc;
+      });
+    v.scoreRangeTotal = acc;
+    return v;
+  });
+  console.log(res.data);
+  courses = res.data;
+}
+
+// const segementsLine = computed(() => {
+//   // console.log(data, data.length);
+//   if (courses.length === 0) return;
+//   return {
+//     tooltip: {
+//       trigger: "axis",
+//       axisPointer: {
+//         type: "shadow",
+//       },
+//     },
+//     title: {
+//       text: courses[0].courseName + "试卷特征量数对比分析",
+//       left: "50%",
+//       textAlign: "center",
+//       bottom: "0px",
+//     },
+//     xAxis: {
+//       type: "category",
+//       data: columns.map((v) => v.title),
+//     },
+//     yAxis: {
+//       type: "value",
+//     },
+//     series: [
+//       {
+//         type: "bar",
+//         stack: "x",
+//         // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+//         data: columns.map((v) => courses[0][v.dataIndex]),
+//       },
+//       {
+//         type: "bar",
+//         stack: "y",
+//         // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+//         data: columns.map((v) => courses[1][v.dataIndex]),
+//       },
+//     ],
+//   } as EChartsOption;
+// });
+
+const columns = [
+  {
+    title: "满分",
+    dataIndex: "totalScore",
+  },
+  {
+    title: "最高分",
+    dataIndex: "maxScore",
+  },
+  {
+    title: "最低分",
+    dataIndex: "minScore",
+  },
+  {
+    title: "全距",
+    dataIndex: "allRange",
+  },
+  {
+    title: "平均分",
+    dataIndex: "avgScore",
+  },
+  {
+    title: "标准差",
+    dataIndex: "stdev",
+  },
+  {
+    title: "差异系数",
+    dataIndex: "coefficient",
+  },
+  {
+    title: "信度1",
+    dataIndex: "reliability1",
+  },
+  {
+    title: "信度2",
+    dataIndex: "reliability2",
+  },
+  {
+    title: "难度",
+    dataIndex: "difficulty",
+  },
+];
+
+onMounted(fetchData);
+
+let scoreGap = $ref(10);
+
+watch(
+  () => [scoreGap, courses],
+  () => {
+    for (const course of courses) {
+      course.segements = [];
+      const validSeg = Math.round(course.totalScore / scoreGap);
+      for (let score = 0; score < validSeg; score++) {
+        const row = [];
+        row[0] = score * scoreGap;
+        let nextScore = score + 1 > validSeg ? course.totalScore : score + 1;
+        // row[1] =
+        //   course.scoreRangeAcc[nextScore * scoreGap - 1] -
+        //   course.scoreRangeAcc[score * scoreGap];
+        row[1] = course.scoreRange
+          .slice(row[0], nextScore * scoreGap)
+          .reduce((p, c) => p + c, 0);
+        row[2] = row[1] / course.scoreRangeAcc[course.totalScore];
+        row[3] = course.scoreRangeAcc[nextScore * scoreGap - 1];
+        row[4] = row[3] / course.scoreRangeAcc[course.totalScore];
+        course.segements.push(row);
+      }
+      if (validSeg * scoreGap === course.totalScore) {
+        course.segements.push([
+          course.totalScore,
+          course.scoreRange[course.totalScore],
+          course.scoreRange[course.totalScore] /
+            course.scoreRangeAcc[course.totalScore],
+          course.scoreRangeAcc[course.totalScore],
+          1,
+        ]);
+      }
+
+      course.rangeSegements = [];
+      for (let i = 0; i < course.rangeConfig.length; i++) {
+        const range = course.rangeConfig[i]!;
+        const nextRange =
+          i === course.rangeConfig.length - 1
+            ? { baseScore: course.totalScore, adjustScore: 0 }
+            : course.rangeConfig[i + 1];
+        const row = [];
+        row[0] = scoreTitle(range);
+        row[1] =
+          course.scoreRange
+            .slice(
+              range.baseScore + range.adjustScore,
+              nextRange.baseScore + nextRange.adjustScore
+            )
+            .reduce((p, c) => p + c, 0) || 0;
+        row[2] = row[1] / course.scoreRangeTotal;
+        row[3] =
+          course.scoreRangeAcc[
+            nextRange.baseScore + nextRange.adjustScore - 1
+          ] || 0;
+        if (
+          nextRange.baseScore + nextRange.adjustScore >=
+          course.scoreRangeAcc.length
+        ) {
+          row[3] = course.scoreRangeAcc[course.scoreRangeAcc.length - 1];
+        }
+        row[4] = row[3] / course.scoreRangeTotal;
+
+        course.rangeSegements.push(row);
+      }
+      // console.log(course);
+    }
+  },
+  {
+    immediate: true,
+  }
+);
+
+// let scores = [];
+// const scoreComputed = computed(() => {
+//   const s = {};
+//   // data[0].scoreRange =
+//   // scores.push()
+// });
+
+function scoreTitle(rangeConfig: RangeConfig) {
+  if (!rangeConfig) return false;
+  if (rangeConfig.type === "ZERO") return "0-";
+
+  return `${rangeConfig.baseScore + rangeConfig.adjustScore}(${
+    RANGE_POINT_TYPE[rangeConfig.type]
+  }${rangeConfig.adjustScore > 0 ? "+" : ""}${rangeConfig.adjustScore})-`;
+}
+
+function segementsLine(course: SasCourse) {
+  console.log(course);
+  return {
+    title: {
+      text: "频率",
+      left: "left",
+    },
+    xAxis: {
+      type: "category",
+      data: course.segements.map((v) => v[0]),
+    },
+    yAxis: {
+      type: "value",
+    },
+    series: [
+      {
+        data: course.segements.map((v) => v[1]),
+        type: "line",
+        smooth: true,
+      },
+    ],
+  } as EChartsOption;
+}
+
+function rangeSegementsLine(course: SasCourse) {
+  console.log(course);
+  return {
+    title: {
+      text: "频率",
+      left: "left",
+    },
+    xAxis: {
+      type: "category",
+      data: course.rangeSegements.map((v) => v[0]),
+    },
+    yAxis: {
+      type: "value",
+    },
+    series: [
+      {
+        data: course.rangeSegements.map((v) => v[1]),
+        type: "line",
+        smooth: true,
+      },
+    ],
+  } as EChartsOption;
+}
+
+const hasFirstTryComparison = $computed(() => {
+  const r = courses
+    .filter((c) => c.totalScoreRangeTitle)
+    .map((c) => JSON.stringify(c.totalScoreRangeTitle));
+
+  const s = new Set(r);
+  return r.length === courses.length && s.size === 1;
+});
+</script>
+
+<style></style>

+ 1 - 0
src/types/index.ts

@@ -65,6 +65,7 @@ export interface SasCourse {
   difficulty: number;
   id: number;
   projectId: number;
+  projectName: string;
   rangeConfig: RangeConfig[];
   scoreRange: number[];
   totalCount: number;