Jelajahi Sumber

整体分析

Michael Wang 3 tahun lalu
induk
melakukan
f6473f5030

+ 2 - 0
auto-imports.d.ts

@@ -37,9 +37,11 @@ declare global {
   const toRefs: typeof import('vue')['toRefs']
   const triggerRef: typeof import('vue')['triggerRef']
   const unref: typeof import('vue')['unref']
+  const useAttrs: typeof import('vue')['useAttrs']
   const useCssModule: typeof import('vue')['useCssModule']
   const useRoute: typeof import('vue-router')['useRoute']
   const useRouter: typeof import('vue-router')['useRouter']
+  const useSlots: typeof import('vue')['useSlots']
   const watch: typeof import('vue')['watch']
   const watchEffect: typeof import('vue')['watchEffect']
 }

+ 7 - 0
components.d.ts

@@ -9,11 +9,13 @@ declare module 'vue' {
     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']
     ARadio: typeof import('ant-design-vue/es')['Radio']
+    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']
@@ -23,9 +25,14 @@ declare module 'vue' {
     ATable: typeof import('ant-design-vue/es')['Table']
     ATabPane: typeof import('ant-design-vue/es')['TabPane']
     ATabs: typeof import('ant-design-vue/es')['Tabs']
+    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']
     'CourseTypeSelect copy': typeof import('./src/components/CourseTypeSelect copy.vue')['default']
+    ExplainModal: typeof import('./src/components/ExplainModal.vue')['default']
     Layout: typeof import('./src/components/Layout.vue')['default']
     PaperTypeSelect: typeof import('./src/components/PaperTypeSelect.vue')['default']
     ProjectCourseSelect: typeof import('./src/components/ProjectCourseSelect.vue')['default']

+ 37 - 0
src/api/allAnalysisPage.ts

@@ -0,0 +1,37 @@
+import { httpApp } from "@/plugins/axiosApp";
+
+/** 试卷特征数分页查询 */
+export function getSasPaperList(params: {
+  courseId?: number;
+  projectId: number;
+  pageNo?: number;
+  pageSize?: number;
+}) {
+  return httpApp.post("/api/ess/sasPaper/page", params);
+}
+
+/** 科目成绩(总分)频率分布-科目成绩占初试总分权重 分页查询 */
+export function getSasCourseList(params: {
+  courseId?: number;
+  projectId: number;
+}) {
+  return httpApp.post("/api/ess/sasCourse/list", params);
+}
+
+/** 科目成绩(总分)频率分布-分段设置 */
+export function setSasCourseRangeConfig(params: {
+  courseId?: number;
+  projectId: number;
+  rangeConfig: any;
+}) {
+  return httpApp.post("/api/ess/sasCourse/rangeConfig", params);
+}
+
+/** 整体分析-科目成绩占初试总分权重-分段设置 */
+export function setSasCourseTotalRangeConfig(params: {
+  courseId?: number;
+  projectId: number;
+  rangeConfig: any;
+}) {
+  return httpApp.post("/api/ess/projectCourse/rangeConfig", params);
+}

+ 147 - 0
src/components/CommonRangeConfig.vue

@@ -0,0 +1,147 @@
+<template>
+  <a-modal
+    title="分段设置"
+    v-model:visible="rangeConfigVisible"
+    @ok="handleOk"
+    ok-text="确定"
+    cancel-text="取消"
+  >
+    <div v-if="selectedRangeConfig">
+      <a-timeline>
+        <a-timeline-item
+          v-for="(item, index) in selectedRangeConfig"
+          :key="index"
+        >
+          {{
+            RANGE_POINT_TYPE[item.type] +
+            "(" +
+            (item.baseScore + item.adjustScore) +
+            ")"
+          }}
+          <a-button
+            v-if="item.type !== 'ZERO'"
+            @click="handleDeleteRangePoint(item)"
+            >删除</a-button
+          >
+        </a-timeline-item>
+      </a-timeline>
+
+      基础分<a-select v-model:value="rangePoint.type" style="width: 100%">
+        <a-select-option value="NATIONAL_SCORE">国家单科线</a-select-option>
+        <a-select-option value="RETEST_SCORE">复试单科线</a-select-option>
+        <a-select-option value="NATIONAL_TOTAL_SCORE"
+          >国家总分线</a-select-option
+        >
+        <a-select-option value="RETEST_TOTAL_SCORE">复试总分线</a-select-option>
+        <a-select-option value="TOTAL_SCORE_LINE">国家满分线</a-select-option>
+        <a-select-option value="CUSTOM">自定义</a-select-option>
+      </a-select>
+      <br />
+      调整分<a-input-number
+        v-model:value="rangePoint.adjustScore"
+      ></a-input-number>
+      <br />
+      <a-button @click="newRangePoint">新增分割点</a-button>
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+import { getProjectCourseList } from "@/api/projectParamsManagementPage";
+import { onMounted, onUpdated, reactive, watch } from "vue-demi";
+
+const emit = defineEmits(["updated"]);
+
+const props =
+  defineProps<{ projectId: number; courseId: number; rangeConfig: any }>();
+
+let rangeConfigVisible = $ref(false);
+const showModal = () => {
+  rangeConfigVisible = true;
+};
+defineExpose({ showModal });
+
+let selectedRangeConfig = $ref([]);
+let courseSetting = $ref({});
+
+const RANGE_POINT_TYPE = {
+  ZERO: "零分",
+  CUSTOM: "自定义",
+  NATIONAL_SCORE: "国家单科线",
+  RETEST_SCORE: "复试单科线",
+  NATIONAL_TOTAL_SCORE: "国家总分线",
+  RETEST_TOTAL_SCORE: "复试总分线",
+  TOTAL_SCORE_LINE: "国家满分线",
+};
+
+watch(
+  () => props.rangeConfig,
+  () => {
+    selectedRangeConfig = props.rangeConfig || [
+      {
+        type: "ZERO",
+        baseScore: 0,
+        adjustScore: 0,
+      },
+    ];
+  }
+);
+
+onUpdated(async () => {
+  const res = await getProjectCourseList({
+    projectId: props.projectId,
+    courseId: props.courseId,
+    pageNo: 1,
+    pageSize: 1,
+  });
+  courseSetting = res.data.content[0];
+});
+
+let rangePoint = reactive({
+  baseScore: 0,
+  adjustScore: 0,
+  type: "",
+});
+
+async function newRangePoint() {
+  if (!rangePoint.adjustScore) {
+    rangePoint.adjustScore = 0;
+  }
+  if (rangePoint.type === "NATIONAL_SCORE") {
+    rangePoint.baseScore = courseSetting.nationalScore;
+  }
+  if (rangePoint.type === "RETEST_SCORE") {
+    rangePoint.baseScore = courseSetting.retestScore;
+  }
+  if (rangePoint.type === "NATIONAL_TOTAL_SCORE") {
+    rangePoint.baseScore = courseSetting.nationalTotalScore;
+  }
+  if (rangePoint.type === "RETEST_TOTAL_SCORE") {
+    rangePoint.baseScore = courseSetting.retestTotalScore;
+  }
+  if (rangePoint.type === "TOTAL_SCORE_LINE") {
+    rangePoint.baseScore = courseSetting.totalScoreLine;
+  }
+  if (rangePoint.type === "ZERO") {
+    rangePoint.baseScore = 0;
+  }
+  selectedRangeConfig.push({ ...rangePoint });
+  selectedRangeConfig = selectedRangeConfig.sort(
+    (a, b) => a.baseScore + a.adjustScore <= b.baseScore + b.adjustScore
+  );
+  Object.assign(rangePoint, {
+    baseScore: 0,
+    adjustScore: 0,
+    type: "",
+  });
+}
+
+function handleDeleteRangePoint(item: any) {
+  selectedRangeConfig = selectedRangeConfig.filter((v) => v !== item);
+}
+
+async function handleOk() {
+  emit("updated", selectedRangeConfig);
+  rangeConfigVisible = false;
+}
+</script>

+ 6 - 1
src/components/ProjectCourseSelect.vue

@@ -5,6 +5,7 @@
     :value="valueStr"
     @change="handleChange"
     style="width: 140px"
+    :disabled="props.disabled"
   >
     <a-select-option
       v-for="(item, index) in optionList"
@@ -20,7 +21,11 @@
 import { getProjectCourseList } from "@/api/projectParamsManagementPage";
 import { onMounted, computed } from "vue-demi";
 
-const props = defineProps<{ value?: null | number; projectId: number }>();
+const props = defineProps<{
+  value?: null | number;
+  projectId: number;
+  disabled?: boolean;
+}>();
 const emit = defineEmits(["update:value"]);
 
 let optionList = $ref<{ courseId: number; courseName: string }[]>([]);

+ 171 - 0
src/features/allAnalysis/AllAnalysis2.vue

@@ -0,0 +1,171 @@
+<template>
+  <div>
+    <div class="tw-bg-white tw-p-5 tw-rounded-xl tw-mb-5">
+      <span class="tw-mr-4"></span>
+      <ProjectSelect :project-id="projectId" v-model:value="projectId" />
+      <ProjectCourseSelect :project-id="projectId" v-model:value="courseId" />
+      <span class="tw-mr-4"></span>
+      <a-button @click="search">查询</a-button>
+
+      <div class="tw-mt-4">
+        <a-button @click="goBack">返回</a-button>
+      </div>
+    </div>
+
+    <div class="tw-bg-white tw-p-5 tw-rounded-xl">
+      <a-radio-group v-model:value="activeTab" class="tw-mb-4">
+        <a-radio-button value="1">试卷特征量数</a-radio-button>
+        <a-radio-button value="2">科目成绩(总分)频率分布</a-radio-button>
+        <a-radio-button value="3">科目成绩占初试总分权重</a-radio-button>
+      </a-radio-group>
+
+      <div v-if="activeTab === '1'">
+        <a-table
+          style="width: 100%; overflow-x: scroll"
+          row-key="id"
+          :columns="columns"
+          :data-source="data"
+          :pagination="{
+          pageSize: pageSize,
+          current: pageNo,
+          total: totalElements,
+          showTotal: (total: number) => `总数:${total}`,
+          onChange: (pageNoChanged: number, pageSizeChanged: number) => {
+            pageNo = pageNoChanged; 
+            pageSize = pageSizeChanged;
+          }
+        }"
+        >
+          <template #course="{ record }">
+            <a>{{ `${record.courseName}(${record.courseCode})` }}</a>
+          </template>
+          <template #action="{ record }">
+            <span>
+              <a-button @click="openModal">说明</a-button>
+              <a-button @click="goPaperAnalysis(record)">查看分析结果</a-button>
+            </span>
+          </template>
+        </a-table>
+      </div>
+
+      <div v-if="activeTab === '2'"><ScoreRate /></div>
+      <div v-if="activeTab === '3'"><ScoreFirstTryRate /></div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useMainStore } from "@/store";
+import { goBack } from "@/utils/utils";
+import { watch, onMounted, ref, toRaw } from "vue-demi";
+import { useRoute } from "vue-router";
+import router from "@/router";
+import { getSasPaperList } from "@/api/allAnalysisPage";
+import EventBus from "@/plugins/eventBus";
+import ScoreRate from "./ScoreRate.vue";
+import ScoreFirstTryRate from "./ScoreFirstTryRate.vue";
+
+const store = useMainStore();
+store.currentLocation = "基础管理 / 整体分析";
+
+let rootOrgId = $ref(undefined as unknown as number);
+let courseId = $ref(undefined as undefined | number);
+const route = useRoute();
+const projectId = +route.params.projectId;
+
+let activeTab = $ref("1");
+
+let data = $ref([]);
+let pageSize = $ref(10);
+let pageNo = $ref(1);
+let totalElements = $ref(0);
+
+async function search() {
+  await fetchData();
+}
+
+watch(() => [pageNo, pageSize], fetchData);
+
+async function fetchData() {
+  const res = await getSasPaperList({ projectId, courseId, pageNo, pageSize });
+  // console.log(res);
+  data = res.data.content;
+  pageNo = res.data.pageNo;
+  pageSize = res.data.pageSize;
+  totalElements = res.data.totalElements;
+}
+
+const columns = [
+  {
+    title: "科目",
+    dataIndex: "course",
+    slots: { customRender: "course" },
+  },
+  {
+    title: "试卷类型",
+    dataIndex: "paperType",
+  },
+  {
+    title: "试卷名称",
+    dataIndex: "paperName",
+  },
+  {
+    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",
+  },
+  {
+    title: "操作",
+    key: "action",
+    slots: { customRender: "action" },
+  },
+];
+
+onMounted(async () => {
+  rootOrgId = store.userInfo.rootOrgId;
+  await search();
+});
+
+async function goPaperAnalysis(record: any) {
+  router.push(`/project/${projectId}/paperAnalysis/${record.paperId}`);
+}
+
+function openModal() {
+  EventBus.emit("SHOW_SETTING", "DESCRIBE01");
+}
+</script>

+ 129 - 0
src/features/allAnalysis/ScoreFirstTryRate.vue

@@ -0,0 +1,129 @@
+<template>
+  <div>
+    <div class="tw-bg-white tw-p-5 tw-rounded-xl tw-mb-5">
+      <ProjectCourseSelect
+        :project-id="projectId"
+        v-model:value="courseId"
+        disabled
+      />
+      <span class="tw-mr-4"></span>
+      <a-button @click="openRangeConfigModal(item)">分段设置</a-button>
+
+      <div class="tw-mt-4"></div>
+
+      <template v-for="(item, index) in data" :key="index">
+        <ProjectCourseSelect
+          :project-id="projectId"
+          v-model:value="item.courseId"
+          disabled
+        />
+        <h3>本科目成绩占初试总分权重</h3>
+        <table>
+          <tr>
+            <th>初试总分分数段</th>
+            <th>人数占比(%)</th>
+            <th>初试总分平均分</th>
+            <th>本科目成绩平均分</th>
+            <th>本科目难度</th>
+            <th>占总分权重(%)</th>
+          </tr>
+          <tr v-for="(item2, index) in item.totalScoreRange" :key="index">
+            <td>
+              {{ item.totalScoreRangeTitle[index] || "全体考生" }}
+            </td>
+            <td>
+              {{ item2.countRate }}
+            </td>
+            <td>
+              {{ item2.avgTotalScore }}
+            </td>
+            <td>
+              {{ item2.avgScore }}
+            </td>
+            <td>
+              {{ item2.difficulty }}
+            </td>
+            <td>
+              {{ item2.scoreRate }}
+            </td>
+          </tr>
+        </table>
+      </template>
+    </div>
+
+    <CommonRangeConfig
+      ref="rangeConfigRef"
+      :project-id="projectId"
+      :course-id="selectedCourseId"
+      :range-config="selectedRangeConfig"
+      @updated="handleRangeConfigUpdate"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useMainStore } from "@/store";
+import { goBack } from "@/utils/utils";
+import { watch, onMounted, ref, toRaw, computed, reactive } from "vue-demi";
+import { useRoute } from "vue-router";
+import {
+  getSasCourseList,
+  setSasCourseTotalRangeConfig,
+} from "@/api/allAnalysisPage";
+import EventBus from "@/plugins/eventBus";
+
+const store = useMainStore();
+store.currentLocation = "基础管理 / 整体分析";
+
+let rootOrgId = $ref(undefined as unknown as number);
+let courseId = $ref(undefined as undefined | number);
+const route = useRoute();
+const projectId = +route.params.projectId;
+
+let data = $ref([]);
+async function fetchData() {
+  const res = await getSasCourseList({
+    projectId,
+    courseId,
+  });
+  // console.log(Object.keys(JSON.parse(res.data[0].scoreRange)));
+  res.data = res.data.map((v: any) => {
+    v.totalScoreRange = JSON.parse(v.totalScoreRange || "{}");
+    v.totalScoreRangeTitle = JSON.parse(v.totalScoreRangeTitle || "{}");
+    return v;
+  });
+  data = res.data;
+}
+
+onMounted(async () => {
+  rootOrgId = store.userInfo.rootOrgId;
+  await fetchData();
+});
+
+let selectedRangeConfig = $ref([]);
+let selectedCourseId = $ref(0);
+
+let rangeConfigRef = $ref(null);
+
+const openRangeConfigModal = (item: any) => {
+  selectedCourseId = item.courseId;
+  console.log(item);
+  selectedRangeConfig = JSON.parse(item.totalRangeConfig || "0") || [
+    {
+      type: "ZERO",
+      baseScore: 0,
+      adjustScore: 0,
+    },
+  ];
+  // @ts-ignore
+  rangeConfigRef.showModal();
+};
+
+async function handleRangeConfigUpdate(rangeConfig: any) {
+  await setSasCourseTotalRangeConfig({
+    courseId: selectedCourseId,
+    projectId: projectId,
+    rangeConfig: JSON.stringify(rangeConfig),
+  });
+}
+</script>

+ 172 - 0
src/features/allAnalysis/ScoreRate.vue

@@ -0,0 +1,172 @@
+<template>
+  <div>
+    <div class="tw-bg-white tw-p-5 tw-rounded-xl tw-mb-5">
+      <span class="tw-mr-4"></span>
+      <ProjectCourseSelect
+        :project-id="projectId"
+        v-model:value="courseId"
+        disabled
+      />
+      <span class="tw-mr-4"></span>
+
+      <div class="tw-mt-4">
+        <a-button @click="goBack">返回</a-button>
+      </div>
+
+      <template v-for="(item, index) in data" :key="index">
+        <ProjectCourseSelect
+          :project-id="projectId"
+          v-model:value="item.courseId"
+          disabled
+        />
+        <h3>等距({{ scoreGap }}分)分组频数分布</h3>
+        <!-- TODO: 将下面的逻辑抽离到JS中,再将分段完成 -->
+        <table>
+          <tr>
+            <th>分数段</th>
+            <th>频数</th>
+            <th>频率(%)</th>
+            <th>累计频数</th>
+            <th>累计频数(%)</th>
+          </tr>
+          <tr v-for="(item2, index) in item.totalScore / scoreGap" :key="index">
+            <td>{{ (item2 - 1) * scoreGap }}-</td>
+            <td>
+              {{
+                item.scoreRangeAcc[item2 * scoreGap] -
+                item.scoreRangeAcc[(item2 - 1) * scoreGap]
+              }}
+            </td>
+            <td>
+              {{
+                (item.scoreRangeAcc[item2 * scoreGap] -
+                  item.scoreRangeAcc[(item2 - 1) * scoreGap]) /
+                item.scoreRangeTotal
+              }}
+            </td>
+            <td>{{ item.scoreRangeAcc[item2 * scoreGap] }}</td>
+            <td>
+              {{ item.scoreRangeAcc[item2 * scoreGap] / item.scoreRangeTotal }}
+            </td>
+          </tr>
+          <tr v-if="item.totalScore % scoreGap">
+            <td>
+              {{
+                item.scoreRangeAcc[item.scoreRangeAcc.length - 1] -
+                item.scoreRangeAcc[
+                  item.scoreRangeAcc.length - 1 - (item.totalScore % scoreGap)
+                ]
+              }}
+            </td>
+            <td>
+              {{
+                (item.scoreRangeAcc[item.scoreRangeAcc.length - 1] -
+                  item.scoreRangeAcc[
+                    item.scoreRangeAcc.length - 1 - (item.totalScore % scoreGap)
+                  ]) /
+                item.scoreRangeTotal
+              }}
+            </td>
+            <td>{{ item.scoreRangeAcc[item.scoreRangeAcc.length - 1] }}</td>
+            <td>100%</td>
+          </tr>
+        </table>
+
+        <h3>科目分数线分组的频数分布</h3>
+        <a-button @click="openRangeConfigModal(item)">分段设置</a-button>
+        <div v-if="item.rangeConfig"></div>
+      </template>
+    </div>
+
+    <CommonRangeConfig
+      ref="rangeConfigRef"
+      :project-id="projectId"
+      :course-id="selectedCourseId"
+      :range-config="selectedRangeConfig"
+      @updated="handleRangeConfigUpdate"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useMainStore } from "@/store";
+import { goBack } from "@/utils/utils";
+import { watch, onMounted, ref, toRaw, computed, reactive } from "vue-demi";
+import { useRoute } from "vue-router";
+import {
+  getSasCourseList,
+  setSasCourseRangeConfig,
+} from "@/api/allAnalysisPage";
+import EventBus from "@/plugins/eventBus";
+
+const store = useMainStore();
+store.currentLocation = "基础管理 / 整体分析";
+
+let rootOrgId = $ref(undefined as unknown as number);
+let courseId = $ref(undefined as undefined | number);
+const route = useRoute();
+const projectId = +route.params.projectId;
+
+let data = $ref([]);
+async function fetchData() {
+  const res = await getSasCourseList({
+    projectId,
+    courseId,
+  });
+  // console.log(Object.keys(JSON.parse(res.data[0].scoreRange)));
+  res.data = res.data.map((v: any) => {
+    v.scoreRange = Object.values(JSON.parse(v.scoreRange || "{}"));
+    return v;
+  });
+  res.data = res.data.map((v: { scoreRange: number[] }) => {
+    let acc = 0;
+    v.scoreRangeAcc = v.scoreRange.map((_v, i, a) => {
+      acc += a[i];
+      return acc;
+    });
+    v.scoreRangeTotal = acc;
+    return v;
+  });
+  data = res.data;
+}
+
+onMounted(async () => {
+  rootOrgId = store.userInfo.rootOrgId;
+  await fetchData();
+});
+
+let scoreGap = 10;
+
+// let scores = [];
+// const scoreComputed = computed(() => {
+//   const s = {};
+//   // data[0].scoreRange =
+//   // scores.push()
+// });
+
+let selectedRangeConfig = $ref([]);
+let selectedCourseId = $ref(0);
+
+let rangeConfigRef = $ref(null);
+
+const openRangeConfigModal = (item: any) => {
+  selectedCourseId = item.courseId;
+  selectedRangeConfig = JSON.parse(item.rangeConfig || "0") || [
+    {
+      type: "ZERO",
+      baseScore: 0,
+      adjustScore: 0,
+    },
+  ];
+  // @ts-ignore
+  rangeConfigRef.showModal();
+};
+
+async function handleRangeConfigUpdate(rangeConfig: any) {
+  await setSasCourseRangeConfig({
+    courseId: selectedCourseId,
+    projectId: projectId,
+    rangeConfig: JSON.stringify(rangeConfig),
+  });
+}
+</script>

File diff ditekan karena terlalu besar
+ 174 - 174
yarn.lock


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini