|
@@ -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>
|