Selaa lähdekoodia

feat: 接口调试

zhangjie 1 vuosi sitten
vanhempi
commit
c7ba769a01

+ 1 - 1
src/modules/course/router.js

@@ -6,7 +6,7 @@ import ProfessionalCertification from "./views/ProfessionalCertification.vue";
 export default [
   {
     path: "/course/target-score-manage",
-    name: "TargetScoreManage",
+    name: "CourseTargetScoreManage",
     component: TargetScoreManage,
   },
   {

+ 109 - 0
src/modules/target/api.js

@@ -220,3 +220,112 @@ export const studentTargetListPage = (datas) => {
 export const studentTargetDetail = (datas) => {
   return $postParam("/api/admin/obe/student_requirement/detail", datas);
 };
+
+// 成绩管理 =====================>
+// 成绩管理
+export const targetScoreListPage = (datas) => {
+  return $postParam("/api/admin/course/degree/score/list", datas);
+};
+// 成绩管理-平时成绩 ------------------->
+// 成绩管理-导入平时成绩-下载模版
+export const scoreTemplateDownload = (datas) => {
+  return $postParam(
+    "/api/admin/course/degree/usual_score/template_download",
+    datas,
+    {
+      responseType: "blob",
+    }
+  );
+};
+// 成绩管理-平时成绩列表
+export const normalScoreListPage = (datas) => {
+  return $postParam("/api/admin/course/degree/usual_score/list", datas);
+};
+// 成绩管理-平时成绩编辑
+export const normalScoreEdit = (datas) => {
+  return $post("/api/admin/course/degree/usual_score/edit", datas);
+};
+// 成绩管理-平时成绩保存
+export const normalScoreSave = (datas) => {
+  return $post("/api/admin/course/degree/usual_score/save", datas);
+};
+// 成绩管理-平时成绩启用/禁用
+export const normalScoreEnable = (datas) => {
+  return $postParam("/api/admin/course/degree/usual_score/enable", datas);
+};
+// 成绩管理-期末成绩 ------------------->
+// 成绩管理-导入期末成绩-下载模版
+export const endScoreTemplateDownload = (datas) => {
+  return $postParam(
+    "/api/admin/course/degree/final_score/template_download",
+    datas,
+    {
+      responseType: "blob",
+    }
+  );
+};
+// 成绩管理-期末成绩列表
+export const endScoreListPage = (datas) => {
+  return $postParam("/api/admin/course/degree/final_score/list", datas);
+};
+// 成绩管理-期末成绩编辑
+export const endScoreEdit = (datas) => {
+  return $post("/api/admin/course/degree/final_score/edit", datas);
+};
+// 成绩管理-期末成绩保存
+export const endScoreSave = (datas) => {
+  return $post("/api/admin/course/degree/final_score/save", datas);
+};
+// 成绩管理-期末成绩同步
+export const endScoreSync = (datas) => {
+  return $postParam("/api/admin/course/degree/final_score/sync", datas);
+};
+// 成绩管理-期末成绩启用/禁用
+export const endScoreEnable = (datas) => {
+  return $postParam("/api/admin/course/degree/final_score/enable", datas);
+};
+// 成绩管理-试卷蓝图详情
+export const endScorePaperPositiveDetail = (datas) => {
+  return $postParam(
+    "/api/admin/course/degree/final_score/paper_struct/query",
+    datas
+  );
+};
+// 成绩管理-保存试卷蓝图
+export const endScorePaperPositiveSave = (datas) => {
+  return $post("/api/admin/course/degree/final_score/paper_struct/save", datas);
+};
+// 成绩管理-同步试卷蓝图
+export const endScorePaperPositiveSync = (datas) => {
+  return $postParam(
+    "/api/admin/course/degree/final_score/paper_struct_dimension/sync",
+    datas
+  );
+};
+// 成绩管理-同步选择试卷
+export const endScoreSyncPaperList = (datas) => {
+  return $postParam("/api/admin/course/degree/final_score/choose_paper", datas);
+};
+
+// 报告管理 ------------------->
+export const targetReportListPage = (datas) => {
+  return $postParam("/api/admin/course/degree/report/list", datas);
+};
+// 报告管理-查看报告
+export const targetReportDetail = (datas) => {
+  return $postParam("/api/admin/course/degree/report/view", datas);
+};
+// 报告管理-保存报告
+export const targetReportSave = (datas) => {
+  return $post("/api/admin/course/degree/report/save", datas);
+};
+// 报告管理-导出报告
+export const exportTargetReport = (datas) => {
+  return $postParam("/api/admin/course/degree/report/export", datas, {
+    responseType: "blob",
+  });
+};
+// 报告管理-报告数据发生变化
+export const targetReportChangeCheck = (datas) => {
+  return $postParam("/api/admin/course/degree/report/change", datas);
+};

+ 103 - 0
src/modules/target/components/target-score/DetailTargetScore.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="target-score-detail">
+    <el-dialog
+      class="page-dialog"
+      :visible.sync="modalIsShow"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      append-to-body
+      fullscreen
+      @open="checkChange"
+    >
+      <div slot="title">
+        成绩管理<span class="color-gray ml-2"
+          >{{ course.courseName }}({{ course.courseCode }})</span
+        >
+      </div>
+      <div class="mb-4 tab-btns">
+        <el-button
+          v-for="tab in tabs"
+          :key="tab.val"
+          size="medium"
+          :type="curTab == tab.val ? 'primary' : 'default'"
+          @click="selectMenu(tab.val)"
+          >{{ tab.name }}
+        </el-button>
+      </div>
+
+      <div v-if="modalIsShow">
+        <component :is="curTab" :course="course"></component>
+      </div>
+
+      <div slot="footer"></div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import TargetScoreNormal from "./TargetScoreNormal.vue";
+import TargetScoreEnd from "./TargetScoreEnd.vue";
+import { targetReportChangeCheck } from "../../api";
+
+export default {
+  name: "target-score-detail",
+  components: {
+    TargetScoreNormal,
+    TargetScoreEnd,
+  },
+  props: {
+    course: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      curTab: "TargetScoreNormal",
+      tabs: [
+        {
+          name: "平时成绩管理",
+          val: "TargetScoreNormal",
+        },
+        {
+          name: "期末成绩管理",
+          val: "TargetScoreEnd",
+        },
+      ],
+    };
+  },
+  methods: {
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    selectMenu(tab) {
+      this.curTab = tab;
+    },
+    async checkChange() {
+      const res = await targetReportChangeCheck({
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
+        report: false,
+      });
+
+      if (res.courseTargetChange) {
+        this.$notify.warning("课程目标与已保存不一致,请重新设置权重!");
+      }
+      if (res.evaluationChange) {
+        this.$notify.warning(
+          "评价方式与已保存不一致,请重新设置权重及导入新的平时成绩!"
+        );
+      }
+      if (res.targetScoreChange) {
+        this.$notify.warning(res.targetScoreChangeStr);
+      }
+    },
+  },
+};
+</script>

+ 148 - 0
src/modules/target/components/target-score/ModifyEndScore.vue

@@ -0,0 +1,148 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    title="期末成绩编辑"
+    top="10vh"
+    width="550px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :key="modalForm.id"
+      label-width="100px"
+    >
+      <el-form-item
+        prop="name"
+        label="考生姓名:"
+        :rules="{
+          required: true,
+          message: '考生姓名不能为空,最多30字符',
+          max: 30,
+          trigger: 'change',
+        }"
+      >
+        <el-input
+          v-model.trim="modalForm.name"
+          placeholder="考生姓名"
+          clearable
+        ></el-input>
+      </el-form-item>
+      <el-form-item label="考生学号:">
+        {{ modalForm.studentCode }}
+      </el-form-item>
+      <el-form-item label="成绩:">
+        {{ totalScore }}
+      </el-form-item>
+      <el-form-item
+        v-for="(item, index) in modalForm.scoreDetail"
+        :key="index"
+        :label="`${item.name}:`"
+        :prop="`scoreDetail.${index}.score`"
+        :rules="{
+          required: true,
+          message: '分数不能为空',
+          trigger: 'change',
+        }"
+      >
+        <el-input-number
+          v-model="item.score"
+          class="width-80"
+          size="small"
+          :min="0"
+          :max="1000"
+          :step="0.01"
+          step-strictly
+          :controls="false"
+        >
+        </el-input-number>
+      </el-form-item>
+    </el-form>
+    <div slot="footer">
+      <el-button type="primary" :disabled="isSubmit" @click="submit"
+        >确认</el-button
+      >
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { calcSum, deepCopy } from "@/plugins/utils";
+import { endScoreSave } from "../../api";
+
+const initModalForm = {
+  id: null,
+  name: "",
+  studentCode: "",
+  score: "",
+  cultureProgramId: "",
+  courseId: "",
+  scoreDetail: [],
+};
+
+export default {
+  name: "ModifyEndScore",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+    };
+  },
+  computed: {
+    totalScore() {
+      return calcSum(this.modalForm.scoreDetail.map((item) => item.score || 0));
+    },
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = this.$objAssign(initModalForm, this.instance);
+      this.modalForm.scoreDetail = deepCopy(this.instance.scoreDetail);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const scoreDetail = this.modalForm.scoreDetail.map((item) => {
+        return { score: item.score + "", name: item.name };
+      });
+      const datas = {
+        id: this.modalForm.id,
+        name: this.modalForm.name,
+        cultureProgramId: this.modalForm.cultureProgramId,
+        courseId: this.modalForm.courseId,
+        score: this.totalScore,
+        scoreDetail: JSON.stringify(scoreDetail),
+      };
+      const data = await endScoreSave(datas).catch(() => {});
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 139 - 0
src/modules/target/components/target-score/ModifyNormalScore.vue

@@ -0,0 +1,139 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    title="平时成绩编辑"
+    top="10vh"
+    width="550px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :key="modalForm.id"
+      label-width="100px"
+    >
+      <el-form-item
+        prop="name"
+        label="考生姓名:"
+        :rules="{
+          required: true,
+          message: '考生姓名不能为空,最多30字符',
+          max: 30,
+          trigger: 'change',
+        }"
+      >
+        <el-input
+          v-model.trim="modalForm.name"
+          placeholder="考生姓名"
+          clearable
+        ></el-input>
+      </el-form-item>
+      <el-form-item label="考生学号:">
+        {{ modalForm.studentCode }}
+      </el-form-item>
+      <el-form-item
+        v-for="(item, index) in modalForm.normalScore"
+        :key="index"
+        :label="`${item.name}:`"
+        :prop="`normalScore.${index}.score`"
+        :rules="{
+          required: true,
+          message: '分数不能为空',
+          trigger: 'change',
+        }"
+      >
+        <el-input-number
+          v-model="item.score"
+          class="width-80"
+          size="small"
+          :min="0"
+          :max="1000"
+          :step="0.01"
+          step-strictly
+          :controls="false"
+        >
+        </el-input-number>
+      </el-form-item>
+    </el-form>
+    <div slot="footer">
+      <el-button type="primary" :disabled="isSubmit" @click="submit"
+        >确认</el-button
+      >
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { deepCopy } from "@/plugins/utils";
+import { normalScoreSave } from "../../api";
+
+const initModalForm = {
+  id: null,
+  name: "",
+  studentCode: "",
+  cultureProgramId: "",
+  courseId: "",
+  normalScore: [],
+};
+
+export default {
+  name: "ModifyNormalScore",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = this.$objAssign(initModalForm, this.instance);
+      this.modalForm.normalScore = deepCopy(this.instance.normalScore);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+
+      const normalScore = this.modalForm.normalScore.map((item) => {
+        return { score: item.score + "", name: item.name };
+      });
+      const datas = {
+        id: this.modalForm.id,
+        name: this.modalForm.name,
+        cultureProgramId: this.modalForm.cultureProgramId,
+        courseId: this.modalForm.courseId,
+        score: JSON.stringify(normalScore),
+      };
+      const data = await normalScoreSave(datas).catch(() => {});
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 128 - 0
src/modules/target/components/target-score/SelectBlueDimensionDialog.vue

@@ -0,0 +1,128 @@
+<template>
+  <el-dialog
+    class="select-blue-dimension-dialog"
+    :visible.sync="modalIsShow"
+    title="选择知识点"
+    top="10vh"
+    width="600px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @opened="visibleChange"
+  >
+    <el-tree
+      v-if="treeData.length"
+      ref="treeRef"
+      :data="treeData"
+      show-checkbox
+      check-on-click-node
+      node-key="id"
+      :props="defaultProps"
+      @check-change="updateTreeStatus"
+    >
+    </el-tree>
+
+    <div slot="footer">
+      <el-button type="primary" @click="submit">确认</el-button>
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: "select-blue-dimension-dialog",
+  props: {
+    selectedData: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    treeData: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      defaultProps: {
+        children: "children",
+        label: "name",
+      },
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.$refs.treeRef.setCheckedKeys(this.selectedData);
+
+      this.$nextTick(() => {
+        this.updateTreeStatus();
+      });
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    updateTreeStatus() {
+      const selectedNodes = this.$refs.treeRef.getCheckedNodes(false, true);
+      const targetNodes = selectedNodes.filter(
+        (item) => item.children && item.children.length
+      );
+      const selectTargetId = targetNodes.length ? targetNodes[0].id : "";
+
+      this.treeData.forEach((item) => {
+        item.disabled = selectTargetId ? item.id !== selectTargetId : false;
+        item.children.forEach((elem) => {
+          elem.disabled = item.disabled;
+        });
+      });
+    },
+    submit() {
+      const selectedNodes = this.$refs.treeRef.getCheckedNodes(false, true);
+
+      if (!selectedNodes.length) {
+        this.$message.error("请选择知识点");
+        return;
+      }
+
+      const targetNodes = selectedNodes.filter(
+        (item) => item.children && item.children.length
+      );
+      if (targetNodes.length > 1) {
+        this.$message.error("只能选择一个课程目标的知识点");
+        return;
+      }
+
+      const dimensionIds = selectedNodes
+        .filter((item) => !item.children)
+        .map((item) => item.id);
+
+      this.$emit(
+        "confirm",
+        targetNodes.map((item) => {
+          return {
+            targetId: item.id,
+            targetName: item.name,
+            dimensionList: item.children
+              .filter((ditem) => dimensionIds.includes(ditem.id))
+              .map((dimension) => {
+                return {
+                  dimensionId: dimension.id,
+                  dimensionCode: dimension.code,
+                  dimensionName: dimension.name,
+                };
+              }),
+          };
+        })
+      );
+      this.cancel();
+    },
+  },
+};
+</script>

+ 264 - 0
src/modules/target/components/target-score/SetBlueDialog.vue

@@ -0,0 +1,264 @@
+<template>
+  <div>
+    <el-dialog
+      :visible.sync="modalIsShow"
+      title="设置试卷蓝图"
+      top="10px"
+      width="660px"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      append-to-body
+      @open="visibleChange"
+    >
+      <div class="mb-2 box-justify">
+        <div class="box-grow mr-2">
+          <span v-for="(target, index) in treeData" :key="target.id">
+            <span>{{ target.name }}占比</span>
+            <span
+              v-if="targetRates[target.id]"
+              :class="[
+                'mlr-1',
+                targetRates[target.id].valid ? 'color-success' : 'color-danger',
+              ]"
+              >{{ targetRates[target.id].rate }}%</span
+            >
+            <span>({{ target.totalWeight || 0 }}%)</span>
+            <span>{{ index === treeData.length - 1 ? "。" : "," }}</span>
+          </span>
+        </div>
+        <el-button type="primary" :loading="loading" @click="toSync"
+          >同步</el-button
+        >
+      </div>
+      <el-table :data="dataList" border height="400">
+        <el-table-column
+          prop="mainNumber"
+          label="大题号"
+          width="80px"
+        ></el-table-column>
+        <el-table-column
+          prop="subNumber"
+          label="小题号"
+          width="80px"
+        ></el-table-column>
+        <el-table-column
+          prop="score"
+          label="小题满分"
+          width="80px"
+        ></el-table-column>
+        <el-table-column prop="courseTargetName" label="所属课程目标">
+        </el-table-column>
+        <!-- <el-table-column prop="dimensionList" label="知识点">
+          <template slot-scope="scope">
+            <template v-for="target in scope.row.targetList">
+              <p
+                v-for="item in target.dimensionList"
+                :key="`${target.targetId}_${item.dimensionId}`"
+              >
+                {{ item.dimensionName }}
+              </p>
+            </template>
+          </template>
+        </el-table-column> -->
+        <el-table-column class-name="action-column" label="操作" width="110px">
+          <template slot-scope="scope">
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toLink(scope.row)"
+              >关联知识点</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+      <div slot="footer">
+        <el-button type="primary" :disabled="isSubmit" @click="submit"
+          >确认</el-button
+        >
+        <el-button @click="cancel">取消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 设置知识点 -->
+    <select-blue-dimension-dialog
+      ref="SelectBlueDimensionDialog"
+      :tree-data="treeData"
+      :selected-data="selectedData"
+      @confirm="dimensionSelected"
+    ></select-blue-dimension-dialog>
+  </div>
+</template>
+
+<script>
+import { calcSum } from "@/plugins/utils";
+import {
+  endScorePaperPositiveDetail,
+  endScorePaperPositiveSave,
+  endScorePaperPositiveSync,
+  courseOutlineTargetListPage,
+} from "../../api";
+import SelectBlueDimensionDialog from "./SelectBlueDimensionDialog.vue";
+
+export default {
+  name: "SetBlueDialog",
+  components: { SelectBlueDimensionDialog },
+  props: {
+    course: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      dataList: [],
+      curRow: {},
+      selectedData: [],
+      treeData: [],
+      targetRates: {},
+      loading: false,
+    };
+  },
+  watch: {
+    "course.obeCourseOutlineId": {
+      immediate: true,
+      handler(val, oldVal) {
+        if (!val) return;
+        if (val !== oldVal) this.getTree();
+      },
+    },
+  },
+  methods: {
+    async getTree() {
+      const data = await courseOutlineTargetListPage({
+        obeCourseOutlineId: this.course.obeCourseOutlineId,
+      });
+      this.treeData = (data || []).map((item) => {
+        return {
+          id: item.id,
+          name: item.targetName,
+          totalWeight: item.totalWeight,
+          disabled: false,
+          children: item.dimensionList.map((elem) => {
+            return { ...elem, disabled: false };
+          }),
+        };
+      });
+    },
+    async getBlueDetail() {
+      const res = await endScorePaperPositiveDetail({
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
+      });
+      this.dataList = res || [];
+      this.updateTargetRates();
+    },
+    visibleChange() {
+      this.getBlueDetail();
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async toSync() {
+      if (this.loading) return;
+      this.loading = true;
+
+      const res = await endScorePaperPositiveSync({
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
+      }).catch(() => {});
+      this.loading = false;
+      if (!res) return;
+
+      this.$message.success(`${res.success},错误:${res.error}`);
+      this.getBlueDetail();
+    },
+    checkData() {
+      const valid = !this.dataList.some(
+        (item) => !item.targetList || !item.targetList.length
+      );
+
+      if (!valid) {
+        this.$message.error("还有小题未设置知识点,请完成设置!");
+        return;
+      }
+
+      const unvalidTargets = [];
+      Object.keys(this.targetRates).forEach((tid) => {
+        const target = this.targetRates[tid];
+        if (!target.valid) unvalidTargets.push(target.name);
+      });
+      if (unvalidTargets.length) {
+        this.$message.error(`${unvalidTargets.join("、")}占比不符合要求`);
+        return;
+      }
+
+      return true;
+    },
+    toLink(row) {
+      this.curRow = row;
+      this.selectedData = [];
+      row.targetList.forEach((target) => {
+        target.dimensionList.forEach((dimension) => {
+          this.selectedData.push(dimension.dimensionId);
+        });
+      });
+      this.$refs.SelectBlueDimensionDialog.open();
+    },
+    dimensionSelected(targetList) {
+      this.curRow.targetList = targetList;
+      this.curRow.courseTargetName = targetList[0].targetName;
+      this.updateTargetRates();
+    },
+    updateTargetRates() {
+      const scoreData = {};
+      this.dataList.forEach((item) => {
+        if (!item.targetList || !item.targetList.length) return;
+        const targetId = item.targetList[0].targetId;
+        if (!scoreData[targetId]) scoreData[targetId] = 0;
+        scoreData[targetId] += item.score;
+      });
+      const totalScore = calcSum(this.dataList.map((item) => item.score));
+
+      const targetRates = {};
+      this.treeData.forEach((target) => {
+        const targetScore = scoreData[target.id] || 0;
+        const rate = !totalScore ? 0 : (100 * targetScore) / totalScore;
+        targetRates[target.id] = {
+          rate: Number.isInteger(rate) ? rate : rate.toFixed(2),
+          valid: rate == target.totalWeight,
+          name: target.name,
+        };
+      });
+      this.targetRates = targetRates;
+    },
+    async submit() {
+      if (!this.checkData()) {
+        return;
+      }
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const datas = {
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
+        paperStruct: this.dataList,
+      };
+      const data = await endScorePaperPositiveSave(datas).catch(() => {});
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 106 - 0
src/modules/target/components/target-score/SyncPaperDialog.vue

@@ -0,0 +1,106 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    title="同步成绩"
+    top="10vh"
+    width="500px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form ref="modalFormComp" :model="modalForm" label-width="60px">
+      <el-form-item
+        prop="paperNumber"
+        label="试卷:"
+        :rules="{
+          required: true,
+          message: '请选择试卷',
+          trigger: 'change',
+        }"
+      >
+        <el-select
+          v-model="modalForm.paperNumber"
+          placeholder="选择试卷"
+          clearable
+          class="width-full"
+        >
+          <el-option
+            v-for="paper in papers"
+            :key="paper.paperNumber"
+            :value="paper.paperNumber"
+            :label="paper.paperNumber"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <div slot="footer">
+      <el-button type="primary" :disabled="isSubmit" @click="submit"
+        >确认</el-button
+      >
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { endScoreSyncPaperList, endScoreSync } from "../../api";
+
+export default {
+  name: "SyncPaperDialog",
+  props: {
+    course: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { paperNumber: "" },
+      papers: [],
+    };
+  },
+  methods: {
+    async getPapers() {
+      if (this.papers.length) return;
+      const res = await endScoreSyncPaperList(this.course);
+      this.papers = res || [];
+    },
+    visibleChange() {
+      this.getPapers();
+      this.modalForm = { paperNumber: "" };
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+
+      const datas = {
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
+        paperNumber: this.modalForm.paperNumber,
+      };
+      const res = await endScoreSync(datas).catch(() => {});
+      this.isSubmit = false;
+
+      if (!res) return;
+
+      this.$message.success(`${res.success},错误:${res.error}`);
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 264 - 0
src/modules/target/components/target-score/TargetScoreEnd.vue

@@ -0,0 +1,264 @@
+<template>
+  <div class="end-score-manage">
+    <div class="part-box part-box-pad box-justify">
+      <p></p>
+      <div>
+        <el-button
+          type="success"
+          :loading="syncing"
+          :disabled="importing"
+          @click="toSync"
+          >同步成绩</el-button
+        >
+        <el-button
+          type="success"
+          :loading="importing"
+          :disabled="syncing"
+          @click="toImportPaperStruct"
+          >导入试卷结构</el-button
+        >
+        <el-button
+          type="success"
+          :loading="importing"
+          :disabled="syncing"
+          @click="toImportEndScore"
+          >导入期末成绩</el-button
+        >
+        <el-button type="success" @click="toSetBlue">设置试卷蓝图</el-button>
+      </div>
+    </div>
+    <div class="part-box part-box-pad">
+      <el-table :data="dataList">
+        <el-table-column type="index" label="序号" width="70"></el-table-column>
+        <el-table-column prop="name" label="姓名" width="140"></el-table-column>
+        <el-table-column
+          prop="studentCode"
+          label="学号"
+          width="140"
+        ></el-table-column>
+        <el-table-column prop="score" label="成绩" width="80">
+        </el-table-column>
+        <el-table-column prop="scoreDetailContent" label="成绩明细">
+        </el-table-column>
+        <el-table-column class-name="action-column" label="操作" width="120px">
+          <template slot-scope="scope">
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              :class="scope.row.enable ? 'btn-danger' : 'btn-primary'"
+              type="text"
+              @click="toEnable(scope.row)"
+              >{{ scope.row.enable ? "禁用" : "启用" }}</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          background
+          layout="total, sizes, prev, pager, next, jumper"
+          :pager-count="5"
+          :current-page="current"
+          :total="total"
+          :page-size="size"
+          @current-change="toPage"
+          @size-change="pageSizeChange"
+        >
+        </el-pagination>
+      </div>
+    </div>
+
+    <!-- ModifyEndScore -->
+    <modify-end-score
+      ref="ModifyEndScore"
+      :instance="curRow"
+      @modified="getList"
+    ></modify-end-score>
+    <!-- ImportEndScore -->
+    <import-file
+      ref="ImportEndScore"
+      title="导入期末成绩"
+      :upload-url="upload.score.uploadUrl"
+      :upload-data="filter"
+      :format="['xls', 'xlsx']"
+      :download-handle="downloadHandle"
+      :download-filename="upload.score.dfilename"
+      :auto-upload="false"
+      :uploading="uploading"
+      @upload-success="uploadSuccess"
+      @upload-error="uploadError"
+    ></import-file>
+    <!-- ImportPaperStruct -->
+    <import-file
+      ref="ImportPaperStruct"
+      title="导入试卷结构"
+      :upload-url="upload.paper.uploadUrl"
+      :upload-data="filter"
+      :format="['xls', 'xlsx']"
+      :download-handle="() => downloadTemplate('paperStruct')"
+      :download-filename="upload.paper.dfilename"
+      :auto-upload="false"
+      :uploading="uploading"
+      @upload-success="uploadSuccess"
+      @upload-error="uploadError"
+    ></import-file>
+    <!-- SetBlueDialog -->
+    <set-blue-dialog ref="SetBlueDialog" :course="course"> </set-blue-dialog>
+    <!-- select papers -->
+    <sync-paper-dialog
+      ref="SyncPaperDialog"
+      :course="filter"
+      @modified="getList"
+    ></sync-paper-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  endScoreListPage,
+  endScoreEnable,
+  endScoreTemplateDownload,
+} from "../../api";
+import ModifyEndScore from "./ModifyEndScore.vue";
+import SetBlueDialog from "./SetBlueDialog.vue";
+import ImportFile from "@/components/ImportFile.vue";
+import SyncPaperDialog from "./SyncPaperDialog.vue";
+import { downloadByApi } from "@/plugins/download";
+import templateDownload from "@/mixins/templateDownload";
+
+export default {
+  name: "end-score-manage",
+  components: { ModifyEndScore, SetBlueDialog, ImportFile, SyncPaperDialog },
+  mixins: [templateDownload],
+  props: {
+    course: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      filter: {
+        cultureProgramId: "",
+        courseId: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+      syncing: false,
+      importing: false,
+      // import
+      upload: {
+        score: {
+          uploadUrl: "/api/admin/course/degree/final_score/import",
+          dfilename: "期末成绩导入模板.xlsx",
+        },
+        paper: {
+          uploadUrl: "/api/admin/course/degree/final_score/paper_struct/import",
+          dfilename: "试卷结构导入模板.xlsx",
+        },
+      },
+      downloading: false,
+    };
+  },
+  mounted() {
+    this.filter = this.$objAssign(this.filter, this.course);
+    this.toPage(1);
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await endScoreListPage(datas);
+      this.dataList = data.records.map((item) => {
+        const nitem = { ...item };
+        if (item.scoreDetail) {
+          nitem.scoreDetail = JSON.parse(item.scoreDetail);
+          nitem.scoreDetailContent = nitem.scoreDetail
+            .map((item) => item.score)
+            .join();
+        }
+
+        return nitem;
+      });
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    toImportEndScore() {
+      this.$refs.ImportEndScore.open();
+    },
+    toImportPaperStruct() {
+      this.$refs.ImportPaperStruct.open();
+    },
+    toSync() {
+      this.$refs.SyncPaperDialog.open();
+    },
+    toSetBlue() {
+      this.$refs.SetBlueDialog.open();
+    },
+    toEdit(row) {
+      this.curRow = { ...row, ...this.filter };
+      this.$refs.ModifyEndScore.open();
+    },
+    uploading() {
+      this.importing = true;
+    },
+    uploadSuccess({ data }) {
+      this.importing = false;
+      const msg = `${data.success},错误:${data.error}`;
+      this.$message.success(msg);
+      this.getList();
+    },
+    uploadError() {
+      this.importing = false;
+    },
+    async downloadHandle() {
+      if (this.downloading) return;
+      this.downloading = true;
+
+      const res = await downloadByApi(() => {
+        return endScoreTemplateDownload(this.filter);
+      }).catch((e) => {
+        this.$message.error(e || "下载失败,请重新尝试!");
+      });
+      this.downloading = false;
+
+      if (!res) return;
+      this.$message.success("下载成功!");
+    },
+    async toEnable(row) {
+      const action = row.enable ? "禁用" : "启用";
+      const confirm = await this.$confirm(
+        `确定要${action}考生【${row.name}】的成绩吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      const enable = !row.enable;
+      await endScoreEnable({
+        id: row.id,
+        enable,
+      });
+      row.enable = enable;
+      this.$message.success("操作成功!");
+    },
+  },
+};
+</script>

+ 203 - 0
src/modules/target/components/target-score/TargetScoreNormal.vue

@@ -0,0 +1,203 @@
+<template>
+  <div class="normal-score-manage">
+    <div class="part-box part-box-pad box-justify">
+      <p>请根据权重管理里权重项导入相应的平时成绩</p>
+      <div>
+        <el-button type="success" @click="toImport">导入平时成绩</el-button>
+      </div>
+    </div>
+    <div class="part-box part-box-pad">
+      <el-table :data="dataList">
+        <el-table-column type="index" label="序号" width="70"></el-table-column>
+        <el-table-column
+          prop="name"
+          label="姓名"
+          minWidth="140"
+        ></el-table-column>
+        <el-table-column
+          prop="studentCode"
+          label="学号"
+          minWidth="140"
+        ></el-table-column>
+        <template v-for="(item, index) in normalScoreItems">
+          <el-table-column :key="index" :label="item">
+            <template slot-scope="scope">
+              {{ scope.row[item] }}
+            </template>
+          </el-table-column>
+        </template>
+        <el-table-column class-name="action-column" label="操作" width="120px">
+          <template slot-scope="scope">
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              :class="scope.row.enable ? 'btn-danger' : 'btn-primary'"
+              type="text"
+              @click="toEnable(scope.row)"
+              >{{ scope.row.enable ? "禁用" : "启用" }}</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          background
+          layout="total, sizes, prev, pager, next, jumper"
+          :pager-count="5"
+          :current-page="current"
+          :total="total"
+          :page-size="size"
+          @current-change="toPage"
+          @size-change="pageSizeChange"
+        >
+        </el-pagination>
+      </div>
+    </div>
+
+    <!-- ModifyNormalScore -->
+    <modify-normal-score
+      ref="ModifyNormalScore"
+      :instance="curRow"
+      @modified="getList"
+    ></modify-normal-score>
+    <!-- ImportFile -->
+    <import-file
+      ref="ImportFile"
+      title="导入平时成绩"
+      :upload-url="uploadUrl"
+      :upload-data="filter"
+      :format="['xls', 'xlsx']"
+      :download-handle="downloadHandle"
+      :download-filename="dfilename"
+      :auto-upload="false"
+      @upload-success="uploadSuccess"
+    ></import-file>
+  </div>
+</template>
+
+<script>
+import {
+  normalScoreListPage,
+  normalScoreEnable,
+  scoreTemplateDownload,
+} from "../../api";
+import ModifyNormalScore from "./ModifyNormalScore.vue";
+import ImportFile from "@/components/ImportFile.vue";
+import { downloadByApi } from "@/plugins/download";
+
+export default {
+  name: "normal-score-manage",
+  components: { ModifyNormalScore, ImportFile },
+  props: {
+    course: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      filter: {
+        cultureProgramId: "",
+        courseId: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+      normalScoreItems: [],
+      // import
+      uploadUrl: "/api/admin/course/degree/usual_score/import",
+      dfilename: "平时成绩导入模板.xlsx",
+      downloading: false,
+    };
+  },
+  async mounted() {
+    this.filter = this.$objAssign(this.filter, this.course);
+    await this.toPage(1);
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await normalScoreListPage(datas);
+      this.dataList = data.records.map((item) => {
+        const nitem = { ...item };
+        if (!item.score) {
+          return nitem;
+        }
+
+        nitem.normalScore = JSON.parse(item.score);
+        nitem.normalScore.forEach((sItem) => {
+          nitem[sItem.name] = sItem.score;
+        });
+        return nitem;
+      });
+      this.total = data.total;
+
+      const score = data.records[0]?.score;
+      if (score) {
+        this.normalScoreItems = JSON.parse(score).map((item) => item.name);
+      }
+    },
+    async toPage(page) {
+      this.current = page;
+      await this.getList();
+    },
+    toImport() {
+      this.$refs.ImportFile.open();
+    },
+    toEdit(row) {
+      this.curRow = { ...row, ...this.filter };
+      this.$refs.ModifyNormalScore.open();
+    },
+    uploadSuccess({ data }) {
+      const msg = `${data.success},错误:${data.error}`;
+      this.$message.success(msg);
+      this.getList();
+    },
+    async downloadHandle() {
+      if (this.downloading) return;
+      this.downloading = true;
+
+      const res = await downloadByApi(() => {
+        return scoreTemplateDownload(this.filter);
+      }).catch((e) => {
+        this.$message.error(e || "下载失败,请重新尝试!");
+      });
+      this.downloading = false;
+
+      if (!res) return;
+      this.$message.success("下载成功!");
+    },
+    async toEnable(row) {
+      const action = row.enable ? "禁用" : "启用";
+      const confirm = await this.$confirm(
+        `确定要${action}考生【${row.name}】的成绩吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      const enable = !row.enable;
+      await normalScoreEnable({
+        id: row.id,
+        enable,
+      });
+      row.enable = enable;
+      this.$message.success("操作成功!");
+    },
+  },
+};
+</script>

+ 11 - 16
src/modules/target/components/target-statistics/DetailTargetStatistics.vue

@@ -11,12 +11,12 @@
   >
     <div slot="title" class="box-justify">
       <div>
-        <span>{{ course.courseName }}({{ course.courseCode }})</span>
+        <span>{{ course.courseName }}</span>
       </div>
       <div>
-        <!-- <el-button type="primary" :loading="downloading" @click="toSave"
+        <el-button type="primary" :loading="downloading" @click="toSave"
           >保存报告</el-button
-        > -->
+        >
         <el-button type="primary" :loading="downloading" @click="toExport"
           >导出报告</el-button
         >
@@ -478,17 +478,15 @@ export default {
 
       await this.checkChange();
       const data = await targetStatisticsDetail({
-        examId: this.course.examId,
-        courseCode: this.course.courseCode,
-        teachCourseId: this.course.teachCourseId,
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
       });
       this.buildData(data);
     },
     async checkChange() {
       const res = await targetStatisticsChangeCheck({
-        examId: this.course.examId,
-        courseCode: this.course.courseCode,
-        teachCourseId: this.course.teachCourseId,
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
         report: true,
       });
 
@@ -856,10 +854,8 @@ export default {
       this.downloading = true;
 
       const res = await targetStatisticsSave({
-        examId: this.course.examId,
-        courseName: this.course.courseName,
-        courseCode: this.course.courseCode,
-        teachCourseId: this.course.teachCourseId,
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
         ...this.courseBasicInfo,
       }).catch(() => {});
       this.downloading = false;
@@ -874,9 +870,8 @@ export default {
 
       const res = await downloadByApi(() => {
         const datas = {
-          examId: this.course.examId,
-          courseCode: this.course.courseCode,
-          teachCourseId: this.course.teachCourseId,
+          cultureProgramId: this.course.cultureProgramId,
+          courseId: this.course.courseId,
         };
         return targetStatisticsReport(datas);
       }).catch((e) => {

+ 6 - 0
src/modules/target/router.js

@@ -4,6 +4,7 @@ import CourseExamine from "./views/CourseExamine.vue";
 import StudentTarget from "./views/StudentTarget.vue";
 import TargetStatistics from "./views/TargetStatistics.vue";
 import RequirementStatistics from "./views/RequirementStatistics.vue";
+import TargetScoreManage from "./views/TargetScoreManage.vue";
 
 export default [
   {
@@ -36,4 +37,9 @@ export default [
     name: "StudentRequirement",
     component: StudentTarget,
   },
+  {
+    path: "/target/target-score-manage",
+    name: "TargetScoreManage",
+    component: TargetScoreManage,
+  },
 ];

+ 149 - 0
src/modules/target/views/TargetScoreManage.vue

@@ -0,0 +1,149 @@
+<template>
+  <div class="target-score-manage">
+    <div class="part-box part-box-filter part-box-flex">
+      <el-form ref="FilterForm" label-position="left" label-width="85px" inline>
+        <template v-if="checkPrivilege('condition', 'condition')">
+          <el-form-item label="培养方案:">
+            <training-plan-select
+              v-model="filter.cultureProgramId"
+              placeholder="培养方案"
+              @change="trainingPlanChange"
+            ></training-plan-select>
+          </el-form-item>
+          <el-form-item label="学期:">
+            <semester-select
+              v-model="filter.semesterId"
+              placeholder="学期"
+            ></semester-select>
+          </el-form-item>
+          <el-form-item label="课程:">
+            <training-plan-course-select
+              v-model="filter.courseId"
+              placeholder="课程"
+              :professional-id="filter.professionalId"
+              :culture-program-id="filter.cultureProgramId"
+            ></training-plan-course-select>
+          </el-form-item>
+        </template>
+        <el-form-item label-width="0px">
+          <el-button
+            v-if="checkPrivilege('button', 'select')"
+            type="primary"
+            @click="search"
+            >查询</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="part-box part-box-pad">
+      <el-table ref="TableList" :data="dataList">
+        <el-table-column
+          type="index"
+          label="序号"
+          width="70"
+          :index="indexMethod"
+        ></el-table-column>
+        <el-table-column label="课程(代码)">
+          <template slot-scope="scope">
+            {{ scope.row.courseName | defaultFieldFilter }}({{
+              scope.row.courseCode | defaultFieldFilter
+            }})
+          </template>
+        </el-table-column>
+        <el-table-column prop="userName" label="创建人">
+          <span slot-scope="scope">
+            {{ scope.row.userName }}({{ scope.row.userLoginName }})
+          </span>
+        </el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          width="100"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              v-if="checkPrivilege('link', 'Score')"
+              class="btn-primary"
+              type="text"
+              @click="toDetail(scope.row)"
+              >管理成绩</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="part-page">
+        <el-pagination
+          background
+          layout="total, sizes, prev, pager, next, jumper"
+          :pager-count="5"
+          :current-page="current"
+          :total="total"
+          :page-size="size"
+          @current-change="toPage"
+          @size-change="pageSizeChange"
+        >
+        </el-pagination>
+      </div>
+    </div>
+    <!-- DetailTargetScore -->
+    <detail-target-score
+      ref="DetailTargetScore"
+      :course="curRow"
+    ></detail-target-score>
+  </div>
+</template>
+
+<script>
+import { targetScoreListPage } from "../api";
+import DetailTargetScore from "../components/target-score/DetailTargetScore.vue";
+
+export default {
+  name: "target-score-manage",
+  components: { DetailTargetScore },
+  data() {
+    return {
+      filter: {
+        semesterId: "",
+        professionalId: "",
+        cultureProgramId: "",
+        courseId: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+    };
+  },
+  methods: {
+    async getList() {
+      if (!this.checkPrivilege("list", "list")) return;
+
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await targetScoreListPage(datas);
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    search() {
+      this.toPage(1);
+    },
+    trainingPlanChange(val) {
+      this.filter.professionalId = val?.professionalId;
+    },
+    toDetail(row) {
+      this.curRow = row;
+      this.$refs.DetailTargetScore.open();
+    },
+  },
+};
+</script>

+ 2 - 2
src/modules/target/views/TargetStatistics.vue

@@ -62,7 +62,7 @@
         >
           <template slot-scope="scope">
             <el-button
-              v-if="checkPrivilege('link', 'detail')"
+              v-if="checkPrivilege('link', 'view')"
               class="btn-primary"
               type="text"
               @click="toDetail(scope.row)"
@@ -87,7 +87,7 @@
     </div>
     <!-- DetailTargetStatistics -->
     <detail-target-statistics
-      v-if="checkPrivilege('link', 'detail')"
+      v-if="checkPrivilege('link', 'view')"
       ref="DetailTargetStatistics"
       :course="curRow"
     ></detail-target-statistics>