zhangjie 1 жил өмнө
parent
commit
c4862644f1
81 өөрчлөгдсөн 9817 нэмэгдсэн , 121 устгасан
  1. 3 2
      package.json
  2. 37 37
      src/assets/styles/adaptive.scss
  3. 18 4
      src/assets/styles/base.scss
  4. 38 0
      src/assets/styles/common-comp.scss
  5. 13 5
      src/assets/styles/element-ui-costom.scss
  6. 23 2
      src/assets/styles/pages.scss
  7. 0 35
      src/components/base/OrgSelect.vue
  8. 75 0
      src/components/base/ProfessionalSelect.vue
  9. 95 0
      src/components/base/TrainingPlanCourseSelect.vue
  10. 85 0
      src/components/base/TrainingPlanSelect.vue
  11. 16 0
      src/constants/enumerate.js
  12. 31 0
      src/modules/base/api.js
  13. 117 0
      src/modules/base/components/ModifyProfessional.vue
  14. 6 0
      src/modules/base/router.js
  15. 176 0
      src/modules/base/views/ProfessionalManage.vue
  16. 6 4
      src/modules/card/components/CardBuildDialog.vue
  17. 1 1
      src/modules/course/components/score/EndScoreManage.vue
  18. 1 1
      src/modules/course/components/score/NormalScoreManage.vue
  19. 3 3
      src/modules/course/components/score/SetBlueDialog.vue
  20. 1 1
      src/modules/course/components/score/SyncPaperDialog.vue
  21. 1 1
      src/modules/course/components/score/TargetScoreDetail.vue
  22. 2 2
      src/modules/course/router.js
  23. 26 6
      src/modules/course/views/TargetScoreManage.vue
  24. 0 3
      src/modules/mark/components/ModifyMarkSetting.vue
  25. 340 0
      src/modules/target/api.js
  26. 124 0
      src/modules/target/components/course-examine/CourseExamineEvaluation.vue
  27. 237 0
      src/modules/target/components/course-examine/CourseExamineWeight.vue
  28. 91 0
      src/modules/target/components/course-examine/DetailCourseExamine.vue
  29. 158 0
      src/modules/target/components/course-examine/ModifyCourseExamineEvaluation.vue
  30. 45 0
      src/modules/target/components/course-outline/CourseOutlineBase.vue
  31. 204 0
      src/modules/target/components/course-outline/CourseOutlineKnowledge.vue
  32. 243 0
      src/modules/target/components/course-outline/CourseOutlineTarget.vue
  33. 93 0
      src/modules/target/components/course-outline/DetailCourseOutline.vue
  34. 233 0
      src/modules/target/components/course-outline/ModifyCourseOutline.vue
  35. 131 0
      src/modules/target/components/course-outline/ModifyCourseOutlineKnowledge.vue
  36. 271 0
      src/modules/target/components/course-outline/ModifyCourseOutlineTarget.vue
  37. 99 0
      src/modules/target/components/course-outline/ModifyCourseOutlineTargetPredict.vue
  38. 293 0
      src/modules/target/components/course-outline/RequirementSelect.vue
  39. 187 0
      src/modules/target/components/course-outline/SelectDimensionDialog.vue
  40. 80 0
      src/modules/target/components/requirement-statistics/DetailRequirementStatistics.vue
  41. 157 0
      src/modules/target/components/requirement-statistics/RequirementStatisticsRadar.vue
  42. 240 0
      src/modules/target/components/requirement-statistics/RequirementStatisticsStatement.vue
  43. 409 0
      src/modules/target/components/student-target/DetailStudentTarget.vue
  44. 103 0
      src/modules/target/components/target-score/DetailTargetScore.vue
  45. 148 0
      src/modules/target/components/target-score/ModifyEndScore.vue
  46. 140 0
      src/modules/target/components/target-score/ModifyNormalScore.vue
  47. 138 0
      src/modules/target/components/target-score/SelectBlueDimensionDialog.vue
  48. 254 0
      src/modules/target/components/target-score/SetBlueDialog.vue
  49. 105 0
      src/modules/target/components/target-score/SyncPaperDialog.vue
  50. 264 0
      src/modules/target/components/target-score/TargetScoreEnd.vue
  51. 203 0
      src/modules/target/components/target-score/TargetScoreNormal.vue
  52. 936 0
      src/modules/target/components/target-statistics/DetailTargetStatistics.vue
  53. 187 0
      src/modules/target/components/training-plan/AddTrainingPlanCourse.vue
  54. 114 0
      src/modules/target/components/training-plan/DetailTrainingPlan.vue
  55. 116 0
      src/modules/target/components/training-plan/ModifyTrainingPlan.vue
  56. 119 0
      src/modules/target/components/training-plan/ModifyTrainingPlanRequirement.vue
  57. 104 0
      src/modules/target/components/training-plan/ModifyTrainingPlanRequirementPredict.vue
  58. 125 0
      src/modules/target/components/training-plan/ModifyTrainingPlanTarget.vue
  59. 45 0
      src/modules/target/components/training-plan/TrainingPlanBase.vue
  60. 146 0
      src/modules/target/components/training-plan/TrainingPlanCourse.vue
  61. 339 0
      src/modules/target/components/training-plan/TrainingPlanCourseMatrix.vue
  62. 84 0
      src/modules/target/components/training-plan/TrainingPlanMatrix.vue
  63. 160 0
      src/modules/target/components/training-plan/TrainingPlanRequirement.vue
  64. 143 0
      src/modules/target/components/training-plan/TrainingPlanTarget.vue
  65. 45 0
      src/modules/target/router.js
  66. 35 0
      src/modules/target/store.js
  67. 185 0
      src/modules/target/views/CourseExamine.vue
  68. 239 0
      src/modules/target/views/CourseOutlineManage.vue
  69. 161 0
      src/modules/target/views/RequirementStatistics.vue
  70. 153 0
      src/modules/target/views/StudentTarget.vue
  71. 168 0
      src/modules/target/views/TargetScoreManage.vue
  72. 157 0
      src/modules/target/views/TargetStatistics.vue
  73. 225 0
      src/modules/target/views/TrainingPlanManage.vue
  74. 2 2
      src/plugins/VueCharts.js
  75. 9 1
      src/plugins/filters.js
  76. 6 0
      src/plugins/globalVuePlugins.js
  77. 44 9
      src/plugins/utils.js
  78. 2 0
      src/router.js
  79. 2 0
      src/store.js
  80. 1 1
      vue.config.js
  81. 1 1
      yarn.lock

+ 3 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "teachcloud-platform-web",
-  "version": "3.3.0",
+  "version": "3.3.4",
   "scripts": {
     "start": "vue-cli-service serve",
     "serve": "vue-cli-service serve",
@@ -18,6 +18,7 @@
     "element-ui": "^2.14.1",
     "js-md5": "^0.7.3",
     "jsbarcode": "^3.11.3",
+    "json5": "^2.2.3",
     "lodash": "^4.17.21",
     "qs": "^6.11.0",
     "vue": "^2.6.14",
@@ -53,4 +54,4 @@
       "git add"
     ]
   }
-}
+}

+ 37 - 37
src/assets/styles/adaptive.scss

@@ -1,37 +1,37 @@
-@media screen and (min-width: 1440px) {
-  // home
-  .home-header {
-    .head-menu {
-      li {
-        padding: 10px 15px;
-        > .icon,
-        > i {
-          margin-right: 8px;
-        }
-      }
-    }
-  }
-  .home-breadcrumb {
-    font-size: $--font-size-base;
-
-    .el-breadcrumb {
-      font-size: $--font-size-base;
-    }
-  }
-  .custom-tree-node {
-    font-size: $--font-size-base;
-  }
-  // element-ui
-  .el-form {
-    &--label-top {
-      .el-form-item__label {
-        padding-bottom: 8px;
-        font-size: $--font-size-base;
-      }
-    }
-  }
-  .el-table,
-  .el-button--small.el-button--text {
-    font-size: $--font-size-base;
-  }
-}
+@media screen and (min-width: 1200px) {
+  // home
+  .home-header {
+    .head-menu {
+      li {
+        padding: 10px 15px;
+        > .icon,
+        > i {
+          margin-right: 8px;
+        }
+      }
+    }
+  }
+  .home-breadcrumb {
+    font-size: $--font-size-base;
+
+    .el-breadcrumb {
+      font-size: $--font-size-base;
+    }
+  }
+  .custom-tree-node {
+    font-size: $--font-size-base;
+  }
+  // element-ui
+  .el-form {
+    &--label-top {
+      .el-form-item__label {
+        padding-bottom: 8px;
+        font-size: $--font-size-base;
+      }
+    }
+  }
+  .el-table,
+  .el-button--small.el-button--text {
+    font-size: $--font-size-base;
+  }
+}

+ 18 - 4
src/assets/styles/base.scss

@@ -173,11 +173,12 @@ body {
     align-items: stretch;
     justify-content: space-between;
     min-height: 30px;
-    margin: -10px 0 10px -10px;
+    margin: 0 0 10px;
     color: $--color-text-dark;
 
     > h3 {
-      font-size: 17px;
+      font-size: 16px;
+      font-weight: 600;
     }
     .el-icon-question {
       margin-left: 10px;
@@ -194,7 +195,6 @@ body {
 .part-title {
   font-size: 16px;
   font-weight: bold;
-  padding: 15px 20px;
   line-height: 30px;
   overflow: hidden;
 
@@ -555,6 +555,11 @@ body {
 .width-100 {
   width: 100px;
 }
+.tooltip-area {
+  max-width: 800px;
+  max-height: 400px;
+  overflow: auto;
+}
 
 // other
 .tips-info {
@@ -598,7 +603,7 @@ body {
   font-size: 12px;
   padding-right: 8px;
 
-  > span {
+  > span:nth-of-type(1) {
     display: block;
     flex-grow: 2;
     word-wrap: break-word;
@@ -614,3 +619,12 @@ body {
   white-space: normal;
   word-break: break-all;
 }
+
+.tooltip-info-icon {
+  color: $--color-text-gray-3;
+  cursor: pointer;
+
+  &:hover {
+    color: $--color-dark;
+  }
+}

+ 38 - 0
src/assets/styles/common-comp.scss

@@ -480,3 +480,41 @@ $--cc-labels-pre: cc-labels;
     }
   }
 }
+// org-select
+.org-select {
+  width: 220px;
+}
+.org-popover {
+  &.el-popover {
+    width: 400px;
+    padding: 0;
+  }
+
+  &-tree {
+    max-height: 300px;
+    overflow: auto;
+    padding: 5px 0;
+  }
+
+  .custom-tree-node {
+    .el-icon-check {
+      display: none;
+    }
+
+    &.is-select {
+      color: #3a5ae5;
+      .el-icon-check {
+        display: block;
+      }
+    }
+
+    &.is-disabled {
+      cursor: not-allowed;
+      color: $--color-text-gray-3;
+    }
+  }
+  .el-tree-node__content {
+    height: auto;
+    min-height: 26px;
+  }
+}

+ 13 - 5
src/assets/styles/element-ui-costom.scss

@@ -177,15 +177,16 @@
       color: $--color-text-gray-2;
     }
   }
-  .el-input__inner {
+
+  &__inner {
     border-radius: 8px;
     border-color: #ddd;
     background-color: #fff;
+
+    &:focus {
+      border-color: $--color-primary;
+    }
   }
-  // .el-input__suffix {
-  //   right: 0;
-  //   border-left: 1px solid #ddd;
-  // }
 }
 // textarea
 .el-textarea {
@@ -665,6 +666,7 @@
     border-left-color: $--color-text-dark-1;
   }
 }
+
 // popper-list
 .popper-list {
   min-width: auto;
@@ -691,3 +693,9 @@
     min-height: 26px;
   }
 }
+
+.el-range-editor.is-active,
+.el-range-editor.is-active:hover,
+.el-select .el-input.is-focus .el-input__inner {
+  border-color: $--color-primary;
+}

+ 23 - 2
src/assets/styles/pages.scss

@@ -1556,11 +1556,32 @@
   }
 }
 
-// professional-matrix
-.professional-matrix {
+// course-matrix
+.course-matrix {
   .el-input-number .el-input__inner {
     padding-left: 5px;
     padding-right: 5px;
     border-radius: 4px;
   }
 }
+
+// normal-score-form
+.normal-score-form {
+  &.el-form {
+    .el-form-item__label {
+      line-height: 20px;
+      padding-top: 6px;
+      padding-bottom: 6px;
+      padding-right: 14px;
+      position: relative;
+
+      &::after {
+        position: absolute;
+        content: ":";
+        right: 0;
+        width: 14px;
+        top: 6px;
+      }
+    }
+  }
+}

+ 0 - 35
src/components/base/OrgSelect.vue

@@ -272,38 +272,3 @@ export default {
   },
 };
 </script>
-
-<style lang="scss">
-.org-select {
-  width: 220px;
-}
-.org-popover {
-  &.el-popover {
-    width: 400px;
-    padding: 0;
-  }
-
-  &-tree {
-    max-height: 300px;
-    overflow: auto;
-    padding: 5px 0;
-  }
-
-  .custom-tree-node {
-    .el-icon-check {
-      display: none;
-    }
-
-    &.is-select {
-      color: #3a5ae5;
-      .el-icon-check {
-        display: block;
-      }
-    }
-  }
-  .el-tree-node__content {
-    height: auto;
-    min-height: 26px;
-  }
-}
-</style>

+ 75 - 0
src/components/base/ProfessionalSelect.vue

@@ -0,0 +1,75 @@
+<template>
+  <el-select
+    v-model="selected"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { conditionProfessionalList } from "../../modules/base/api";
+
+export default {
+  name: "professional-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true },
+    defaultSelect: { type: Boolean, default: false },
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: "",
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      },
+    },
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      this.optionList = [];
+      const res = await conditionProfessionalList();
+      this.optionList = res;
+
+      if (this.defaultSelect && !this.value) this.selectDefault();
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find((item) => item.id === this.selected)
+      );
+    },
+    selectDefault() {
+      const defaultData = this.optionList[0];
+      if (defaultData) {
+        this.selected = defaultData.id;
+        this.$emit("input", this.selected);
+        this.$emit("change", defaultData);
+        this.$emit("default-selected", defaultData);
+      }
+    },
+  },
+};
+</script>

+ 95 - 0
src/components/base/TrainingPlanCourseSelect.vue

@@ -0,0 +1,95 @@
+<template>
+  <el-select
+    v-model="selected"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { conditionTrainingPlanCourseList } from "../../modules/base/api";
+
+export default {
+  name: "training-plan-course-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true },
+    professionalId: { type: String, default: "" },
+    cultureProgramId: { type: String, default: "" },
+    defaultSelect: { type: Boolean, default: false },
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: "",
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      },
+    },
+    professionalId(val, oldval) {
+      if (val !== oldval) {
+        this.search();
+        this.$emit("input", "");
+        this.$emit("change", {});
+      }
+    },
+    cultureProgramId(val, oldval) {
+      if (val !== oldval) {
+        this.search();
+        this.$emit("input", "");
+        this.$emit("change", {});
+      }
+    },
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      this.optionList = [];
+      if (!this.professionalId || !this.cultureProgramId) return;
+      const res = await conditionTrainingPlanCourseList({
+        professionalId: this.professionalId,
+        cultureProgramId: this.cultureProgramId,
+      });
+      this.optionList = res;
+
+      if (this.defaultSelect && !this.value) this.selectDefault();
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find((item) => item.id === this.selected)
+      );
+    },
+    selectDefault() {
+      const defaultData = this.optionList[0];
+      if (defaultData) {
+        this.selected = defaultData.id;
+        this.$emit("input", this.selected);
+        this.$emit("change", defaultData);
+        this.$emit("default-selected", defaultData);
+      }
+    },
+  },
+};
+</script>

+ 85 - 0
src/components/base/TrainingPlanSelect.vue

@@ -0,0 +1,85 @@
+<template>
+  <el-select
+    v-model="selected"
+    :placeholder="placeholder"
+    filterable
+    :clearable="clearable"
+    :disabled="disabled"
+    @change="select"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.id"
+      :value="item.id"
+      :label="item.name"
+    >
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { conditionTrainingPlanList } from "../../modules/base/api";
+
+export default {
+  name: "training-plan-select",
+  props: {
+    disabled: { type: Boolean, default: false },
+    placeholder: { type: String, default: "请选择" },
+    value: { type: [Number, String], default: "" },
+    clearable: { type: Boolean, default: true },
+    professionalId: { type: String },
+    professionalRequired: { type: Boolean, default: false },
+    defaultSelect: { type: Boolean, default: false },
+  },
+  data() {
+    return {
+      optionList: [],
+      selected: "",
+    };
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.selected = val;
+      },
+    },
+    professionalId(val, oldval) {
+      if (val !== oldval && this.professionalRequired) {
+        this.search();
+        this.$emit("input", "");
+        this.$emit("change", {});
+      }
+    },
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    async search() {
+      this.optionList = [];
+      if (this.professionalRequired && !this.professionalId) return;
+      const res = await conditionTrainingPlanList(this.professionalId);
+      this.optionList = res;
+
+      if (this.defaultSelect && !this.value) this.selectDefault();
+    },
+    select() {
+      this.$emit("input", this.selected);
+      this.$emit(
+        "change",
+        this.optionList.find((item) => item.id === this.selected)
+      );
+    },
+    selectDefault() {
+      const defaultData = this.optionList[0];
+      if (defaultData) {
+        this.selected = defaultData.id;
+        this.$emit("input", this.selected);
+        this.$emit("change", defaultData);
+        this.$emit("default-selected", defaultData);
+      }
+    },
+  },
+};
+</script>

+ 16 - 0
src/constants/enumerate.js

@@ -399,3 +399,19 @@ export const SCORE_POLICY_TYPE = {
   MAX: "最高分",
   MIN: "最低分",
 };
+
+// target
+export const EVALUATION_MODE = {
+  THEORY_EXAMINATION: "理论考试类",
+  EXAMINE: "考查类",
+  COURSE_DESIGN: "课程设计类",
+  GRADUATE_DESIGN: "毕业设计类",
+  OTHER: "其他",
+};
+export const COURSE_TYPE = {
+  ENGINEERING_BASIC: "工程基础类课程",
+  MAJOR_BASIC: "专业基础类课程",
+  MAJOR: "专业类课程",
+  ENGINEERING_PRACTICE_AND_GRADUATE_DESIGN: "工程实践与毕业设计(论文)",
+  OTHER: "其他",
+};

+ 31 - 0
src/modules/base/api.js

@@ -63,6 +63,26 @@ export const conditionListClazz = ({ semesterId, examId, courseId }) => {
     courseId,
   });
 };
+// professional
+export const conditionProfessionalList = () => {
+  return $postParam("/api/admin/basic/condition/list_professional", {});
+};
+// training-plan
+export const conditionTrainingPlanList = (professionalId) => {
+  return $postParam("/api/admin/basic/condition/list_culture_program", {
+    professionalId,
+  });
+};
+// training-plan-course
+export const conditionTrainingPlanCourseList = ({
+  professionalId,
+  cultureProgramId,
+}) => {
+  return $postParam("/api/admin/basic/condition/list_culture_program_course", {
+    professionalId,
+    cultureProgramId,
+  });
+};
 
 // user --------------------------------->
 // user-manage
@@ -620,3 +640,14 @@ export const exportExamStudent = (datas) => {
     responseType: "blob",
   });
 };
+
+// 专业管理 ------------------->
+export const professionalListPage = (datas) => {
+  return $postParam("/api/admin/basic/professional/page", datas);
+};
+export const deleteProfessional = (id) => {
+  return $postParam("/api/admin/basic/professional/remove", { id });
+};
+export const updateProfessional = (datas) => {
+  return $post("/api/admin/basic/professional/save", datas);
+};

+ 117 - 0
src/modules/base/components/ModifyProfessional.vue

@@ -0,0 +1,117 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    :title="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"
+      :key="modalForm.id"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-form-item prop="name" label="专业名称:">
+        <el-input
+          v-model.trim="modalForm.name"
+          placeholder="专业名称"
+          clearable
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="orgId" label="所属学院:">
+        <org-select
+          v-model="modalForm.orgId"
+          placeholder="所属学院"
+          :filter-param="{ orgId: instance.orgId, withoutPrintingRoom: true }"
+        ></org-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 { updateProfessional } from "../api";
+
+const initModalForm = {
+  id: null,
+  orgId: "",
+  name: "",
+};
+
+export default {
+  name: "ModifyProfessional",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      rules: {
+        name: [
+          { required: true, message: "请输入专业名称", trigger: "change" },
+          {
+            message: "专业名称不能超过30个字",
+            max: 30,
+            trigger: "change",
+          },
+        ],
+        orgId: [
+          { required: true, message: "请选择所属学院", trigger: "change" },
+        ],
+      },
+    };
+  },
+  computed: {
+    isEdit() {
+      return !!this.instance.id;
+    },
+    title() {
+      return (this.isEdit ? "编辑" : "新增") + "专业";
+    },
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = this.$objAssign(initModalForm, this.instance);
+    },
+    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 data = await updateProfessional(this.modalForm).catch(() => {});
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

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

@@ -22,6 +22,7 @@ import ExamManage from "./views/ExamManage.vue";
 import CourseSimpleManage from "./views/CourseSimpleManage.vue";
 import ClazzSimpleManage from "./views/ClazzSimpleManage.vue";
 import ExamStudentManage from "./views/ExamStudentManage.vue";
+import ProfessionalManage from "./views/ProfessionalManage.vue";
 
 export default [
   {
@@ -139,4 +140,9 @@ export default [
     name: "BasicExamStudentManage",
     component: ExamStudentManage,
   },
+  {
+    path: "/base/professional-manage",
+    name: "ProfessionalManage",
+    component: ProfessionalManage,
+  },
 ];

+ 176 - 0
src/modules/base/views/ProfessionalManage.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="specialty-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="所属学院:">
+            <org-select
+              v-model="filter.orgId"
+              placeholder="所属学院"
+              :filter-param="{ orgId: userOrgId, withoutPrintingRoom: true }"
+            ></org-select>
+          </el-form-item>
+          <el-form-item label="专业名称:">
+            <el-input
+              v-model.trim="filter.name"
+              placeholder="专业名称"
+              clearable
+            ></el-input>
+          </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 class="part-box-action">
+        <el-button
+          v-if="checkPrivilege('button', 'add')"
+          type="primary"
+          icon="el-icon-add"
+          @click="toAdd"
+          >新增</el-button
+        >
+      </div>
+    </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 prop="name" label="专业"> </el-table-column>
+        <el-table-column prop="orgName" label="所属学院"> </el-table-column>
+        <el-table-column prop="createName" label="创建人">
+          <span slot-scope="scope">
+            {{ scope.row.userName }}({{ scope.row.loginName }})
+          </span>
+        </el-table-column>
+        <!-- <el-table-column prop="createTime" label="创建时间">
+          <span slot-scope="scope">
+            {{ scope.row.createTime | timestampFilter }}
+          </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', 'edit')"
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              v-if="checkPrivilege('link', 'delete')"
+              class="btn-danger"
+              type="text"
+              @click="toDelete(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>
+    <!-- ModifyProfessional -->
+    <modify-professional
+      v-if="checkPrivilege('button', 'add')"
+      ref="ModifyProfessional"
+      :instance="curRow"
+      @modified="getList"
+    ></modify-professional>
+  </div>
+</template>
+
+<script>
+import { professionalListPage, deleteProfessional } from "../api";
+import ModifyProfessional from "../components/ModifyProfessional.vue";
+
+export default {
+  name: "specialty-manage",
+  components: { ModifyProfessional },
+  data() {
+    return {
+      filter: {
+        orgId: "",
+        name: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+      userOrgId: this.$ls.get("orgId", ""),
+    };
+  },
+  mounted() {
+    this.toPage(1);
+  },
+  methods: {
+    async getList() {
+      if (!this.checkPrivilege("list", "list")) return;
+
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await professionalListPage(datas);
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    search() {
+      this.toPage(1);
+    },
+    toAdd() {
+      this.curRow = {
+        orgId: this.userOrgId,
+      };
+      this.$refs.ModifyProfessional.open();
+    },
+    toEdit(row) {
+      this.curRow = row;
+      this.$refs.ModifyProfessional.open();
+    },
+    toDelete(row) {
+      this.$confirm(`确定要删除专业【${row.name}】吗?`, "提示", {
+        type: "warning",
+      })
+        .then(async () => {
+          await deleteProfessional(row.id);
+          this.$message.success("删除成功!");
+          this.deletePageLastItem();
+        })
+        .catch(() => {});
+    },
+  },
+};
+</script>

+ 6 - 4
src/modules/card/components/CardBuildDialog.vue

@@ -68,6 +68,7 @@
 </template>
 
 <script>
+import json5 from "json5";
 import { mapState, mapMutations, mapActions } from "vuex";
 import { saveCard, cardConfigInfos } from "../api";
 import { tikuPaperDetail } from "../../exam/api";
@@ -78,7 +79,7 @@ import { getCardHeadModel } from "../../../../card/elementModel";
 import TopicElementPreview from "../../../../card/components/TopicElementPreview";
 import CardView from "../../../../card/components/CardView.vue";
 import CardHeadSample from "../../../../card/elements/card-head/CardHead";
-import { deepCopy, objTypeOf, removeRichTextValue } from "@/plugins/utils";
+import { deepCopy, objTypeOf } from "@/plugins/utils";
 // ceshi
 // import paperData from "./paper.json";
 
@@ -154,11 +155,12 @@ export default {
           attachmentId: res.attachmentId,
         };
         const answerJson = res.answerJson
-          ? JSON.parse(removeRichTextValue(res.answerJson))
+          ? json5.parse(res.answerJson)
           : { details: [] };
         const paperJson = res.paperJson
-          ? JSON.parse(removeRichTextValue(res.paperJson))
-          : {};
+          ? json5.parse(res.paperJson)
+          : { details: [] };
+
         this.rebuildPaperQuestionNumber(answerJson);
         this.rebuildPaperQuestionNumber(paperJson);
         this.parsePaperAnswers(paperJson, answerJson);

+ 1 - 1
src/modules/course/components/score/EndScoreManage.vue

@@ -145,7 +145,7 @@ export default {
   data() {
     return {
       filter: {
-        examId: "",
+        cultureProgramId: "",
         courseId: "",
         teachCourseId: "",
       },

+ 1 - 1
src/modules/course/components/score/NormalScoreManage.vue

@@ -103,7 +103,7 @@ export default {
   data() {
     return {
       filter: {
-        examId: "",
+        cultureProgramId: "",
         courseId: "",
         teachCourseId: "",
       },

+ 3 - 3
src/modules/course/components/score/SetBlueDialog.vue

@@ -150,7 +150,7 @@ export default {
     },
     async getBlueDetail() {
       const res = await endScorePaperPositiveDetail({
-        examId: this.course.examId,
+        cultureProgramId: this.course.cultureProgramId,
         courseId: this.course.courseId,
         teachCourseId: this.course.teachCourseId,
       });
@@ -171,7 +171,7 @@ export default {
       this.loading = true;
 
       const res = await endScorePaperPositiveSync({
-        examId: this.course.examId,
+        cultureProgramId: this.course.cultureProgramId,
         courseId: this.course.courseId,
         teachCourseId: this.course.teachCourseId,
         paperNumber: this.dataList[0]?.paperNumber,
@@ -249,7 +249,7 @@ export default {
       if (this.isSubmit) return;
       this.isSubmit = true;
       const datas = {
-        examId: this.course.examId,
+        cultureProgramId: this.course.cultureProgramId,
         courseId: this.course.courseId,
         teachCourseId: this.course.teachCourseId,
         paperStruct: this.dataList,

+ 1 - 1
src/modules/course/components/score/SyncPaperDialog.vue

@@ -88,7 +88,7 @@ export default {
       this.isSubmit = true;
 
       const datas = {
-        examId: this.course.examId,
+        cultureProgramId: this.course.cultureProgramId,
         courseId: this.course.courseId,
         teachCourseId: this.course.teachCourseId,
         paperNumber: this.modalForm.paperNumber,

+ 1 - 1
src/modules/course/components/score/TargetScoreDetail.vue

@@ -81,7 +81,7 @@ export default {
     },
     async checkChange() {
       const res = await targetReportChangeCheck({
-        examId: this.course.examId,
+        cultureProgramId: this.course.cultureProgramId,
         courseId: this.course.courseId,
         paperNumber: this.course.paperNumber,
         teachCourseId: this.course.teachCourseId,

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

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

+ 26 - 6
src/modules/course/views/TargetScoreManage.vue

@@ -3,11 +3,27 @@
     <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')">
-          <secp-select
-            v-model="filter"
-            defaultSelectExam
-            @exam-default="search"
-          ></secp-select>
+          <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
@@ -90,7 +106,8 @@ export default {
     return {
       filter: {
         semesterId: "",
-        examId: "",
+        professionalId: "",
+        cultureProgramId: "",
         courseId: "",
       },
       current: 1,
@@ -120,6 +137,9 @@ export default {
     search() {
       this.toPage(1);
     },
+    trainingPlanChange(val) {
+      this.filter.professionalId = val?.professionalId;
+    },
     toDetail(row) {
       this.curRow = row;
       this.$refs.TargetScoreDetail.open();

+ 0 - 3
src/modules/mark/components/ModifyMarkSetting.vue

@@ -33,7 +33,6 @@
           <el-date-picker
             v-model="markTime"
             type="datetimerange"
-            :picker-options="pickerOptions"
             range-separator="至"
             start-placeholder="评卷开始日期"
             end-placeholder="评卷结束日期"
@@ -128,7 +127,6 @@ import { cardDetail } from "../../card/api";
 
 import { MARK_MODE_TYPE } from "@/constants/enumerate";
 import ModifyPaperArea from "./ModifyPaperArea.vue";
-import pickerOptions from "@/constants/datePickerOptions";
 
 const initModalForm = {
   examId: null,
@@ -201,7 +199,6 @@ export default {
       },
       // date-picker
       markTime: [],
-      pickerOptions,
     };
   },
   computed: {

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

@@ -0,0 +1,340 @@
+import { $postParam, $post } from "@/plugins/axios";
+
+// 培养方案管理相关页面 =====================>
+// 培养方案管理 ------------------->
+export const trainingPlanListPage = (datas) => {
+  return $postParam("/api/admin/obe/culture/program/page", datas);
+};
+export const deleteTrainingPlan = (id) => {
+  return $postParam("/api/admin/obe/culture/program/remove", { id });
+};
+export const copyTrainingPlan = (id) => {
+  return $postParam("/api/admin/obe/culture/program/copy", { id });
+};
+export const updateTrainingPlan = (datas) => {
+  return $post("/api/admin/obe/culture/program/save", datas);
+};
+export const updateTrainingPlanDescription = (datas) => {
+  return $post("/api/admin/obe/culture/program/description/save", datas);
+};
+export const updateTrainingPlanDetail = (id) => {
+  return $postParam("/api/admin/obe/culture/program/get", { id });
+};
+// 培养方案管理-培养目标 ------------------->
+export const trainingPlanTargetListPage = (datas) => {
+  return $postParam("/api/admin/obe/culture/program/target/list", datas);
+};
+export const deleteTrainingPlanTarget = (id) => {
+  return $postParam("/api/admin/obe/culture/program/target/remove", { id });
+};
+export const updateTrainingPlanTarget = (datas) => {
+  return $post("/api/admin/obe/culture/program/target/save", datas);
+};
+// 培养方案管理-毕业要求 ------------------->
+export const trainingPlanRequirementListPage = (datas) => {
+  return $postParam("/api/admin/obe/culture/program/requirement/list", datas);
+};
+export const addTrainingPlanRequirementNode = (datas) => {
+  return $postParam(
+    "/api/admin/obe/culture/program/requirement/node/add",
+    datas
+  );
+};
+export const deleteTrainingPlanRequirementNode = (id) => {
+  return $postParam("/api/admin/obe/culture/program/requirement/remove", {
+    id,
+  });
+};
+export const updateTrainingPlanRequirement = (datas) => {
+  return $post("/api/admin/obe/culture/program/requirement/save", datas);
+};
+export const updateTrainingPlanRequirementPredict = (datas) => {
+  return $post("/api/admin/obe/culture/program/requirement/expect/save", datas);
+};
+// 培养方案管理-培养目标与毕业要求关系矩阵 ------------------->
+export const trainingPlanMatrixListPage = (datas) => {
+  return $postParam("/api/admin/obe/culture/program/target/matrix/get", datas);
+};
+export const updateRrainingPlanMatrix = (datas) => {
+  return $post("/api/admin/obe/culture/program/target/matrix/save", datas);
+};
+// 培养方案管理-课程体系 ------------------->
+export const trainingPlanCourseListPage = (datas) => {
+  return $postParam("/api/admin/obe/culture/program/course/list", datas);
+};
+export const deleteRrainingPlanCourse = (id) => {
+  return $postParam("/api/admin/obe/culture/program/course/remove", { id });
+};
+export const sortRrainingPlanCourse = (datas) => {
+  return $post("/api/admin/obe/culture/program/course/sort", datas);
+};
+export const trainingPlanCourseQueryList = (datas) => {
+  return $postParam("/api/admin/obe/culture/program/course/query_list", datas);
+};
+export const trainingPlanCourseSave = (datas) => {
+  return $post("/api/admin/obe/culture/program/course/save", datas);
+};
+// 培养方案管理-课程支撑毕业要求达成矩阵 ------------------->
+export const trainingPlanCourseMatrixDetail = (datas) => {
+  return $postParam("/api/admin/obe/culture/program/course/matrix/get", datas);
+};
+export const trainingPlanCourseMatrixSave = (datas) => {
+  return $post("/api/admin/obe/culture/program/course/matrix/save", datas, {
+    silence: true,
+  });
+};
+
+// 课程大纲管理 ------------------->
+export const courseOutlineListPage = (datas) => {
+  return $postParam("/api/admin/obe/course_outline/page", datas);
+};
+export const deleteCourseOutline = (id) => {
+  return $postParam("/api/admin/obe/course_outline/delete", { id });
+};
+export const updateCourseOutline = (datas) => {
+  return $post("/api/admin/obe/course_outline/save", datas);
+};
+export const selectableTrainingPlanList = () => {
+  return $postParam(
+    "/api/admin/obe/course_outline/can_choose_culture_program",
+    {}
+  );
+};
+export const selectableTrainingPlanCourseList = (cultureProgramId) => {
+  return $postParam("/api/admin/obe/course_outline/can_choose_course", {
+    cultureProgramId,
+  });
+};
+// 课程大纲管理-课程目标 ------------------->
+export const courseOutlineTargetListPage = (datas) => {
+  return $postParam("/api/admin/obe/course_target/list", datas);
+};
+export const deleteCourseOutlineTarget = (id) => {
+  return $postParam("/api/admin/obe/course_target/delete", { id });
+};
+export const updateCourseOutlineTarget = (datas) => {
+  return $post("/api/admin/obe/course_target/save", datas);
+};
+export const updateCourseOutlineTargetPredict = (datas) => {
+  return $post("/api/admin/obe/course_target/setting_expect_value", datas);
+};
+export const courseOutlineTargetRequirementList = (datas) => {
+  return $postParam("/api/admin/obe/course_target/sub_requirement/list", datas);
+};
+export const courseOutlineTargetKnowledgeList = (datas) => {
+  return $postParam("/api/admin/obe/course_target/dimension_tree", datas);
+};
+export const courseOutlineTargetRequirementKnowledge = (datas) => {
+  return $postParam(
+    "/api/admin/obe/course_target/requirement_dimension",
+    datas
+  );
+};
+
+// 暂时不做
+// 课程大纲管理-知识点 ------------------->
+// export const courseOutlineKnowledgeListPage = (datas) => {
+//   return $postParam("/api/admin/obe/culture/program/course/list", datas);
+// };
+// export const deleteCourseOutlineKnowledge = (id) => {
+//   return $postParam("/api/admin/obe/culture/program/course/remove", { id });
+// };
+// export const sortCourseOutlineKnowledge = (datas) => {
+//   return $post("/api/admin/obe/culture/program/course/sort", datas);
+// };
+// export const updateCourseOutlineKnowledge = (datas) => {
+//   return $post("/api/admin/basic/professional/save", datas);
+// };
+
+// 课程考核设置 ------------------->
+export const courseExamineListPage = (datas) => {
+  return $postParam(
+    "/api/admin/obe/course_outline/assessment_setting_page",
+    datas
+  );
+};
+export const deleteCourseExamine = (id) => {
+  return $postParam("/api/admin/obe/course_outline/assessment_setting_delete", {
+    id,
+  });
+};
+// 课程考核设置-评价方式 ------------------->
+export const courseExamineEvaluationListPage = (datas) => {
+  return $postParam("/api/admin/obe/course_evaluation/list", datas);
+};
+export const deleteCourseExamineEvaluation = (id) => {
+  return $postParam("/api/admin/obe/course_evaluation/delete", { id });
+};
+export const updateCourseExamineEvaluation = (datas) => {
+  return $post("/api/admin/obe/course_evaluation/save", datas);
+};
+export const courseExamineWeightSettingStatus = (datas) => {
+  return $postParam(
+    "/api/admin/obe/course_outline/find_course_weight_setting_status",
+    datas
+  );
+};
+// 课程考核设置-权重设置 ------------------->
+export const courseExamineWeightDetail = (datas) => {
+  return $postParam("/api/admin/obe/course_weight/find", datas);
+};
+export const courseExamineWeightSave = (datas) => {
+  return $post("/api/admin/obe/course_weight/save", datas);
+};
+
+// 统计相关页面 =====================>
+// 毕业要求达成度统计 ------------------->
+export const requirementStatisticsListPage = (datas) => {
+  return $postParam("/api/admin/obe/requirements/list", datas);
+};
+export const requirementStatisticsRadar = (datas) => {
+  return $postParam("/api/admin/obe/requirements/radar/report", datas);
+};
+export const requirementStatisticsDetail = (datas) => {
+  return $postParam("/api/admin/obe/requirements/detail/report", datas);
+};
+export const requirementStatisticsCalculate = (datas) => {
+  return $postParam("/api/admin/obe/requirements/again/calculate", datas);
+};
+
+// 课程目标达成度统计 ------------------->
+export const targetStatisticsListPage = (datas) => {
+  return $postParam("/api/admin/course/degree/report/list", datas);
+};
+export const targetStatisticsDetail = (datas) => {
+  return $postParam("/api/admin/course/degree/report/view", datas);
+};
+export const targetStatisticsSave = (datas) => {
+  return $post("/api/admin/course/degree/report/save", datas);
+};
+export const targetStatisticsReport = (datas) => {
+  return $postParam("/api/admin/course/degree/report/export", datas, {
+    responseType: "blob",
+  });
+};
+export const targetStatisticsChangeCheck = (datas) => {
+  return $postParam("/api/admin/course/degree/report/change", datas);
+};
+
+// 学生毕业要求达成度 ------------------->
+export const studentTargetListPage = (datas) => {
+  return $postParam("/api/admin/obe/student_requirement/page", datas);
+};
+export const studentTargetDetail = (datas) => {
+  return $postParam("/api/admin/obe/student_requirement/detail", datas);
+};
+export const studentTargetCalculate = (datas) => {
+  return $postParam(
+    "/api/admin/obe/student_requirement/again/calculate",
+    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);
+};

+ 124 - 0
src/modules/target/components/course-examine/CourseExamineEvaluation.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="course-evaluation-manage">
+    <div class="part-box part-box-pad box-justify">
+      <p></p>
+      <div>
+        <el-button type="primary" @click="toAdd">自定义评价方式</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="evaluation"
+          label="评价方式"
+          min-width="120"
+        ></el-table-column>
+        <el-table-column
+          prop="evaluationDesc"
+          label="评价方式描述"
+          min-width="300"
+        >
+        </el-table-column>
+        <el-table-column class-name="action-column" label="操作" width="120px">
+          <template slot-scope="scope">
+            <el-button
+              v-if="scope.row.type === 'CUSTOM'"
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              v-if="scope.row.type === 'CUSTOM'"
+              class="btn-danger"
+              type="text"
+              @click="toDelete(scope.row)"
+              >删除</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- ModifyCourseExamineEvaluation -->
+    <modify-course-examine-evaluation
+      ref="ModifyCourseExamineEvaluation"
+      :instance="curRow"
+      @modified="getList"
+    ></modify-course-examine-evaluation>
+  </div>
+</template>
+
+<script>
+import {
+  courseExamineEvaluationListPage,
+  deleteCourseExamineEvaluation,
+} from "../../api";
+import ModifyCourseExamineEvaluation from "./ModifyCourseExamineEvaluation.vue";
+
+export default {
+  name: "course-examine-evaluation",
+  components: { ModifyCourseExamineEvaluation },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      dataList: [],
+      curRow: {},
+    };
+  },
+  mounted() {
+    this.getList();
+  },
+  methods: {
+    async getList() {
+      const res = await courseExamineEvaluationListPage({
+        obeCourseOutlineId: this.rowData.id,
+      });
+      this.dataList = res || [];
+    },
+    toAdd() {
+      this.curRow = {
+        obeCourseOutlineId: this.rowData.id,
+      };
+      this.$refs.ModifyCourseExamineEvaluation.open();
+    },
+    toEdit(row) {
+      this.curRow = {
+        obeCourseOutlineId: this.rowData.id,
+        ...row,
+      };
+      this.$refs.ModifyCourseExamineEvaluation.open();
+    },
+    async toDelete(row) {
+      const confirm = await this.$confirm(
+        `删除评价方式会影响权重设置,确定要删除评价方式【${row.evaluation}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      const res = await deleteCourseExamineEvaluation(row.evaluationId);
+      if (res.warning) {
+        this.$notify({
+          title: "警告",
+          message: "评价方式有变化,请重新设置权重设置!",
+          type: "warning",
+          duration: 5000,
+        });
+      }
+      this.$message.success("删除成功!");
+      this.getList();
+    },
+  },
+};
+</script>

+ 237 - 0
src/modules/target/components/course-examine/CourseExamineWeight.vue

@@ -0,0 +1,237 @@
+<template>
+  <div class="course-weight-manage course-matrix">
+    <div class="part-box part-box-pad">
+      <el-table :data="dataList" :summary-method="getSummaries" show-summary>
+        <el-table-column
+          prop="courseTargetName"
+          label="课程目标"
+          width="200"
+          fixed="left"
+        ></el-table-column>
+        <el-table-column label="考核项目及比例(%)" align="center">
+          <el-table-column
+            v-for="(column, cindex) in columns"
+            :key="cindex"
+            :label="column"
+            align="center"
+            min-width="80"
+          >
+            <template slot-scope="scope">
+              <el-input-number
+                v-model="scope.row.evaluationList[cindex].weight"
+                :disabled="scope.row.evaluationList[cindex].disabled"
+                class="width-50"
+                size="small"
+                :min="0"
+                :max="100"
+                :step="1"
+                step-strictly
+                :controls="false"
+                @blur="weightChange(scope.$index, cindex)"
+              >
+              </el-input-number>
+            </template>
+          </el-table-column>
+        </el-table-column>
+        <el-table-column label="权重(%)" prop="totalWeight" align="center">
+        </el-table-column>
+      </el-table>
+      <div class="text-center mt-2">
+        <el-button
+          class="width-200"
+          type="primary"
+          :loading="loading"
+          @click="submit"
+          >保存</el-button
+        >
+      </div>
+
+      <div>
+        <p>说明:</p>
+        <p>
+          1.课程目标评价依据来源于平时成绩和期末考试成绩二部分,请录入各考核项目的权重;
+        </p>
+        <p>2.各考核项目整体权重应等于100%,用于计算课程整体达成度;</p>
+        <p>
+          3.期末考试可以分布在不同课程目标下,其他过程性考核项目一个仅能支持一个课程目标。
+        </p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { courseExamineWeightDetail, courseExamineWeightSave } from "../../api";
+import { calcSum } from "@/plugins/utils";
+
+export default {
+  name: "course-examine-weight",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      dataList: [],
+      columns: [],
+      loading: false,
+    };
+  },
+  mounted() {
+    this.initData();
+  },
+  methods: {
+    async initData() {
+      this.dataList = [];
+      this.columns = [];
+      await this.getList();
+    },
+    async getList() {
+      const res = await courseExamineWeightDetail({
+        obeCourseOutlineId: this.rowData.id,
+      });
+      const dataList = res.submitForm || [];
+
+      if (!dataList[0]) return;
+
+      this.columns = dataList[0].evaluationList.map(
+        (item) => item.evaluationName
+      );
+
+      this.columns.forEach((column, cindex) => {
+        if (cindex === 0) {
+          dataList.forEach((item, eindex) => {
+            const node = item.evaluationList[cindex];
+            node.disabled = false;
+            node.enable = !!node.weight;
+            node.weight = node.weight || undefined;
+          });
+          return;
+        }
+
+        const rowIndex = dataList.findIndex((item, eindex) => {
+          return !!item.evaluationList[cindex].weight;
+        });
+        if (rowIndex !== -1) {
+          dataList.forEach((item, eindex) => {
+            const node = item.evaluationList[cindex];
+            node.disabled = eindex !== rowIndex;
+            node.weight = node.weight || undefined;
+          });
+        } else {
+          dataList.forEach((item, eindex) => {
+            const node = item.evaluationList[cindex];
+            node.disabled = false;
+            node.weight = node.weight || undefined;
+          });
+        }
+      });
+
+      this.dataList = dataList;
+    },
+    weightChange(rowIndex, cindex) {
+      this.dataList.forEach((item, tindex) => {
+        item.totalWeight = calcSum(
+          item.evaluationList.map((elem) => elem.weight || 0)
+        );
+      });
+
+      if (cindex === 0) {
+        this.dataList.forEach((item, eindex) => {
+          const node = item.evaluationList[cindex];
+          node.enable = !!node.weight;
+        });
+        return;
+      }
+
+      const val = this.dataList[rowIndex].evaluationList[cindex].weight;
+      if (val) {
+        this.dataList.forEach((item, eindex) => {
+          const node = item.evaluationList[cindex];
+          node.disabled = eindex !== rowIndex;
+          node.enable = eindex === rowIndex;
+        });
+      } else {
+        this.dataList.forEach((item, eindex) => {
+          const node = item.evaluationList[cindex];
+          node.disabled = false;
+          node.enable = false;
+        });
+      }
+    },
+    checkDataList() {
+      if (!this.dataList.length) return;
+
+      // 所有课程目标都应该有设置权重
+      this.dataList.forEach((item, tindex) => {
+        item.totalWeight = calcSum(
+          item.evaluationList.map((elem) => elem.weight || 0)
+        );
+      });
+      const unvalidData = this.dataList
+        .filter((item) => !item.totalWeight)
+        .map((item) => item.courseTargetName);
+
+      if (unvalidData.length) {
+        this.$message.error(`请设置${unvalidData.join(",")}的权重`);
+        return;
+      }
+
+      // 目标整体权重
+      const totalWeight = calcSum(
+        this.dataList.map((item) => item.totalWeight || 0)
+      );
+      if (totalWeight !== 100) {
+        this.$message.error("目标整体权重合计不等于100%");
+        return;
+      }
+
+      return true;
+    },
+    async submit() {
+      if (this.loading) return;
+      if (!this.checkDataList()) return;
+
+      this.loading = true;
+      const res = await courseExamineWeightSave({
+        obeCourseOutlineId: this.rowData.id,
+        submitForm: this.dataList,
+      }).catch(() => {});
+      this.loading = false;
+      if (!res) return;
+
+      this.initData();
+      this.$message.success("保存成功!");
+    },
+    getSummaries(param) {
+      const { columns } = param;
+      const sums = [];
+      const lastNo = columns.length - 1;
+      columns.forEach((column, index) => {
+        if (index === 0) {
+          sums[index] = "合计";
+          return;
+        }
+
+        if (index === lastNo) {
+          sums[index] = calcSum(
+            this.dataList.map((item) => item.totalWeight || 0)
+          );
+          return;
+        }
+
+        sums[index] = calcSum(
+          this.dataList.map(
+            (item) => item.evaluationList[index - 1].weight || 0
+          )
+        );
+      });
+      return sums;
+    },
+  },
+};
+</script>

+ 91 - 0
src/modules/target/components/course-examine/DetailCourseExamine.vue

@@ -0,0 +1,91 @@
+<template>
+  <el-dialog
+    class="page-dialog"
+    :visible.sync="modalIsShow"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    fullscreen
+    @close="closeHandle"
+    @open="visibleChange"
+  >
+    <div slot="title">{{ rowData.outlineName }}</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" :row-data="rowData"></component>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import CourseExamineEvaluation from "./CourseExamineEvaluation.vue";
+import CourseExamineWeight from "./CourseExamineWeight.vue";
+import { mapActions } from "vuex";
+
+export default {
+  name: "detail-course-examine",
+  components: {
+    CourseExamineEvaluation,
+    CourseExamineWeight,
+  },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      curTab: "CourseExamineEvaluation",
+      tabs: [
+        {
+          name: "评价方式管理",
+          val: "CourseExamineEvaluation",
+        },
+        {
+          name: "权重设置",
+          val: "CourseExamineWeight",
+        },
+      ],
+    };
+  },
+  methods: {
+    ...mapActions("target", ["updateCwStatus"]),
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    visibleChange() {
+      this.updateCwStatus({
+        obeCourseOutlineId: this.rowData.id,
+      });
+    },
+    selectMenu(tab) {
+      this.curTab = tab;
+      this.updateCwStatus({
+        obeCourseOutlineId: this.rowData.id,
+      });
+    },
+    closeHandle() {
+      this.curTab = "CourseExamineEvaluation";
+    },
+  },
+};
+</script>

+ 158 - 0
src/modules/target/components/course-examine/ModifyCourseExamineEvaluation.vue

@@ -0,0 +1,158 @@
+<template>
+  <el-dialog
+    class="modify-semester"
+    :visible.sync="modalIsShow"
+    :title="title"
+    top="10vh"
+    width="700px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      label-width="120px"
+      label-position="top"
+    >
+      <el-form-item prop="evaluation" label="评价方式:">
+        <el-input
+          v-model.trim="modalForm.evaluation"
+          placeholder="请输入评价方式"
+          clearable
+          :disabled="isEdit"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="evaluationDesc" label="评价方式描述:">
+        <el-input
+          v-model="modalForm.evaluationDesc"
+          placeholder="请输入评价方式描述"
+          type="textarea"
+          :autosize="{ minRows: 4, maxRows: 10 }"
+          :maxlength="999"
+          resize="none"
+          show-word-limit
+        ></el-input>
+      </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 { updateCourseExamineEvaluation } from "../../api";
+import { mapState } from "vuex";
+
+const initModalForm = {
+  evaluationId: null,
+  obeCourseOutlineId: "",
+  evaluation: "",
+  evaluationDesc: "",
+};
+
+export default {
+  name: "modify-course-examine-evaluation",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  computed: {
+    ...mapState("target", ["cwStatus"]),
+    isEdit() {
+      return !!this.instance.evaluationId;
+    },
+    title() {
+      return (this.isEdit ? "编辑" : "新增") + "评价方式";
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      rules: {
+        evaluation: [
+          {
+            required: true,
+            message: "请输入评价方式",
+            trigger: "change",
+          },
+          {
+            message: "评价方式不能超过30个字",
+            max: 30,
+            trigger: "change",
+          },
+        ],
+        evaluationDesc: [
+          {
+            required: true,
+            max: 999,
+            message: "评价方式描述不能超过999个字",
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  methods: {
+    initData(val) {
+      this.modalForm = this.$objAssign(initModalForm, val);
+    },
+    visibleChange() {
+      this.initData(this.instance);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.cwStatus.everSettingStatus) {
+        const confirm = await this.$confirm(
+          `${this.title}会影响权重设置,确定要${this.title}吗?`,
+          "提示",
+          {
+            type: "warning",
+          }
+        ).catch(() => {});
+        if (confirm !== "confirm") return;
+      }
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      let datas = { ...this.modalForm, id: this.modalForm.evaluationId };
+      const res = await updateCourseExamineEvaluation(datas).catch(() => {});
+      this.isSubmit = false;
+
+      if (!res) return;
+
+      this.$message.success(this.title + "成功!");
+      if (res.warning) {
+        this.$notify({
+          title: "警告",
+          message: "评价方式有变化,请重新设置权重设置!",
+          type: "warning",
+          duration: 5000,
+        });
+      }
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 45 - 0
src/modules/target/components/course-outline/CourseOutlineBase.vue

@@ -0,0 +1,45 @@
+<template>
+  <div class="course-outline-base part-box part-box-pad">
+    <el-descriptions title="课程大纲" :column="1" size="medium">
+      <el-descriptions-item label="课程大纲名称">
+        {{ rowData.outlineName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="课程名称">
+        {{ rowData.courseName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="学分">
+        {{ rowData.credit }}
+      </el-descriptions-item>
+      <el-descriptions-item label="考核方式">
+        {{ rowData.evaluationMode | evaluationModeFilter }}
+      </el-descriptions-item>
+      <el-descriptions-item label="课程类别">
+        {{ rowData.courseType | courseTypeFilter }}
+      </el-descriptions-item>
+      <el-descriptions-item label="修读学期">
+        {{ rowData.semesterName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="所属培养方案">
+        {{ rowData.cultureProgramName }}
+      </el-descriptions-item>
+    </el-descriptions>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "course-outline-base",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>

+ 204 - 0
src/modules/target/components/course-outline/CourseOutlineKnowledge.vue

@@ -0,0 +1,204 @@
+<template>
+  <div class="course-outline-knowledge">
+    <div class="part-box part-box-pad">
+      <div class="box-justify mb-2">
+        <div></div>
+        <div>
+          <el-button type="primary" @click="toAdd">新增知识点</el-button>
+        </div>
+      </div>
+      <el-table
+        ref="TableList"
+        :data="dataList"
+        row-key="id"
+        default-expand-all
+      >
+        <el-table-column label="知识点"> </el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          width="140"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row, 'up')"
+              >编辑</el-button
+            >
+            <el-button
+              class="btn-danger"
+              type="text"
+              @click="toDelete(scope.row)"
+              >删除</el-button
+            >
+            <el-button
+              v-if="checkMoveValid(scope.row, 'up')"
+              class="btn-primary"
+              type="text"
+              @click="toMove(scope.row, 'up')"
+              >上移</el-button
+            >
+            <el-button
+              v-if="checkMoveValid(scope.row, 'down')"
+              class="btn-primary"
+              type="text"
+              @click="toMove(scope.row, 'down')"
+              >下移</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- ModifyCourseOutlineKnowledge -->
+    <modify-course-outline-knowledge
+      ref="ModifyCourseOutlineKnowledge"
+      :instance="curRow"
+      :main-options="mainOptions"
+      @modified="getList"
+    ></modify-course-outline-knowledge>
+  </div>
+</template>
+
+<script>
+import {
+  courseOutlineKnowledgeListPage,
+  deleteCourseOutlineKnowledge,
+  sortCourseOutlineKnowledge,
+} from "../../api";
+
+import ModifyCourseOutlineKnowledge from "./ModifyCourseOutlineKnowledge.vue";
+
+export default {
+  name: "course-outline-knowledge",
+  components: {
+    ModifyCourseOutlineKnowledge,
+  },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      dataList: [],
+      curRow: {},
+      mainOptions: [],
+    };
+  },
+  mounted() {
+    this.getList();
+  },
+  methods: {
+    async getList() {
+      const data = await courseOutlineKnowledgeListPage({
+        obeCourseOutlineId: this.rowData.id,
+      });
+      this.dataList = data || [];
+      this.mainOptions = this.dataList.map((item) => {
+        return {
+          id: item.id,
+          name: item.name,
+        };
+      });
+    },
+    toAdd() {
+      this.curRow = {};
+      this.$refs.ModifyCourseOutlineKnowledge.open();
+    },
+    toEdit(row) {
+      this.curRow = row;
+      this.$refs.ModifyCourseOutlineKnowledge.open();
+    },
+    exchangeSortNumber(row1, row2) {
+      const sortNum = row1.sortNum;
+      row1.sortNum = row2.sortNum;
+      row2.sortNum = sortNum;
+    },
+    checkMoveValid(row, type) {
+      if (row.children) {
+        const rowIndex = this.dataList.findIndex((item) => item.id === row.id);
+        if (type === "up") {
+          return rowIndex !== 0;
+        } else {
+          return rowIndex !== this.dataList.length - 1;
+        }
+      } else {
+        let row = {};
+        let rowIndex = 0;
+        this.dataList.forEach((item) => {
+          item.children.forEach((elem, index) => {
+            if (elem.id === row.id) {
+              row = item;
+              rowIndex = index;
+            }
+          });
+        });
+        if (type === "up") {
+          return rowIndex !== 0;
+        } else {
+          return rowIndex !== row.children.length - 1;
+        }
+      }
+    },
+    toMove(row, type) {
+      if (row.children) {
+        const rowIndex = this.dataList.findIndex((item) => item.id === row.id);
+        this.toMoveRow(this.dataList, rowIndex, type);
+      } else {
+        let row = {};
+        let rowIndex = 0;
+        this.dataList.forEach((item) => {
+          item.children.forEach((elem, index) => {
+            if (elem.id === row.id) {
+              row = item;
+              rowIndex = index;
+            }
+          });
+        });
+        this.toMoveRow(row.children, rowIndex, type);
+      }
+    },
+    async toMoveRow(dataList, rowIndex, type) {
+      const row1 = dataList[rowIndex];
+      const row2 =
+        type === "up" ? dataList[rowIndex - 1] : dataList[rowIndex + 1];
+
+      this.exchangeSortNumber(row1, row2);
+
+      const datas = dataList.map((item, index) => {
+        return {
+          id: item.id,
+          sortNum: item.sortNum,
+        };
+      });
+
+      const res = await sortCourseOutlineKnowledge(datas).catch(() => {
+        this.exchangeSortNumber(row1, row2);
+      });
+      if (!res) return;
+
+      this.dataList.sort((a, b) => a.sortNum - b.sortNum);
+    },
+    async toDelete(row) {
+      const confirm = await this.$confirm(
+        `确定要删除知识点【${row.courseName}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await deleteCourseOutlineKnowledge(row.id);
+      this.$message.success("删除成功!");
+      this.getList();
+    },
+  },
+};
+</script>

+ 243 - 0
src/modules/target/components/course-outline/CourseOutlineTarget.vue

@@ -0,0 +1,243 @@
+<template>
+  <div class="course-outline-target">
+    <div class="part-box part-box-pad">
+      <div class="box-justify mb-2">
+        <div></div>
+        <div>
+          <el-button type="primary" @click="toAdd">新增课程目标</el-button>
+          <el-button type="success" @click="toPredict">预期值</el-button>
+        </div>
+      </div>
+      <el-table ref="TableList" :data="dataList">
+        <el-table-column
+          type="index"
+          label="序号"
+          width="70"
+          :index="indexMethod"
+        ></el-table-column>
+        <el-table-column prop="targetName" label="课程目标" min-width="120">
+        </el-table-column>
+        <el-table-column prop="expectValue" label="预期值" width="120">
+        </el-table-column>
+        <el-table-column prop="degreeRequirement" label="内容" min-width="300">
+        </el-table-column>
+        <el-table-column
+          prop="obeCultureProgramRequirementName"
+          label="指标点"
+          width="140"
+        >
+          <template slot-scope="scope">
+            <span>{{ scope.row.obeCultureProgramRequirementName }}</span>
+            <el-tooltip
+              v-if="scope.row.obeCultureProgramRequirementContent"
+              effect="dark"
+              placement="top"
+            >
+              <div slot="content" class="tooltip-area">
+                {{ scope.row.obeCultureProgramRequirementContent }}
+              </div>
+              <i class="el-icon-info ml-1 tooltip-info-icon"></i>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          width="120"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              class="btn-danger"
+              type="text"
+              @click="toDelete(scope.row)"
+              >删除</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="mt-2">
+        <span>未被选择的指标点:</span>
+        <span>{{ unuseRequirements.join(",") || "无" }}</span>
+        <!-- <el-tag
+          v-for="(item, index) in unuseRequirements"
+          :key="index"
+          class="mr-1"
+          type="danger"
+          >{{ item }}</el-tag
+        > -->
+      </div>
+    </div>
+
+    <!-- ModifyCourseOutlineTarget -->
+    <modify-course-outline-target
+      ref="ModifyCourseOutlineTarget"
+      :instance="curRow"
+      :dimensions="dimensions"
+      :requirement-list="requirementList"
+      @modified="targetModified"
+    ></modify-course-outline-target>
+    <!-- ModifyCourseOutlineTargetPredict -->
+    <modify-course-outline-target-predict
+      ref="ModifyCourseOutlineTargetPredict"
+      :rowData="rowData"
+      @modified="getList"
+    ></modify-course-outline-target-predict>
+  </div>
+</template>
+
+<script>
+import {
+  courseOutlineTargetListPage,
+  deleteCourseOutlineTarget,
+  courseOutlineTargetRequirementList,
+} from "../../api";
+import ModifyCourseOutlineTarget from "./ModifyCourseOutlineTarget.vue";
+import ModifyCourseOutlineTargetPredict from "./ModifyCourseOutlineTargetPredict.vue";
+
+export default {
+  name: "course-outline-target",
+  components: { ModifyCourseOutlineTarget, ModifyCourseOutlineTargetPredict },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalForm: {
+        description: "",
+      },
+      dataList: [],
+      curRow: {},
+      requirementList: [],
+      unuseRequirements: [],
+      dimensions: [],
+    };
+  },
+  mounted() {
+    this.initData();
+  },
+  methods: {
+    async initData() {
+      await this.getList();
+      await this.getRequirementList();
+      this.updateUnuseRequirements();
+    },
+    async getList() {
+      const datas = {
+        obeCourseOutlineId: this.rowData.id,
+      };
+      const data = await courseOutlineTargetListPage(datas);
+      this.dataList = data || [];
+      this.updateRequirementDimensions();
+    },
+    updateRequirementDimensions() {
+      const data = {};
+      this.dataList.forEach((item) => {
+        const k = item.obeCultureProgramRequirementId;
+        if (data[k]) return;
+        data[k] = item.dimensionList.map((elem) => elem.id);
+      });
+      this.dimensions = Object.keys(data).map((k) => {
+        return {
+          requirementId: k,
+          dimensionIds: data[k],
+        };
+      });
+    },
+    updateUnuseRequirements() {
+      const usedRids = this.dataList.map(
+        (item) => item.obeCultureProgramRequirementId
+      );
+      const unusedData = [];
+      this.requirementList.forEach((item) => {
+        item.children.forEach((elem) => {
+          if (usedRids.includes(elem.id)) return;
+          unusedData.push(elem.name);
+        });
+      });
+      this.unuseRequirements = unusedData;
+    },
+    async getRequirementList() {
+      const data = await courseOutlineTargetRequirementList({
+        cultureProgramId: this.rowData.cultureProgramId,
+        courseId: this.rowData.courseId,
+      });
+      this.requirementList = (data || []).map((item) => {
+        item.children = item.subRequirements.map((elem) => {
+          const nelem = { ...elem, disabled: false };
+          nelem.parentName = item.name;
+          return nelem;
+        });
+        item.subRequirements = null;
+        return item;
+      });
+    },
+    updateRequirementList(obeCultureProgramRequirementId) {
+      const usedIds = this.dataList
+        .filter(
+          (item) =>
+            item.obeCultureProgramRequirementId !==
+            obeCultureProgramRequirementId
+        )
+        .map((item) => item.obeCultureProgramRequirementId);
+
+      this.requirementList.forEach((item) => {
+        item.children.forEach((elem) => {
+          elem.disabled = usedIds.includes(elem.id);
+        });
+      });
+    },
+    toAdd() {
+      this.curRow = {
+        obeCourseOutlineId: this.rowData.id,
+        cultureProgramId: this.rowData.cultureProgramId,
+        courseId: this.rowData.courseId,
+      };
+      this.updateRequirementList();
+      this.$refs.ModifyCourseOutlineTarget.open();
+    },
+    toPredict() {
+      this.$refs.ModifyCourseOutlineTargetPredict.open();
+    },
+    toEdit(row) {
+      this.curRow = {
+        ...row,
+        obeCourseOutlineId: this.rowData.id,
+        cultureProgramId: this.rowData.cultureProgramId,
+        courseId: this.rowData.courseId,
+      };
+      this.updateRequirementList(row.obeCultureProgramRequirementId);
+      this.$refs.ModifyCourseOutlineTarget.open();
+    },
+    async targetModified() {
+      await this.getList();
+      this.updateUnuseRequirements();
+    },
+    async toDelete(row) {
+      const confirm = await this.$confirm(
+        `修改课程目标会影响权重设置,确定要删除课程目标【${row.targetName}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await deleteCourseOutlineTarget(row.id);
+      this.$message.success("删除成功!");
+      this.targetModified();
+    },
+  },
+};
+</script>

+ 93 - 0
src/modules/target/components/course-outline/DetailCourseOutline.vue

@@ -0,0 +1,93 @@
+<template>
+  <el-dialog
+    class="page-dialog"
+    :visible.sync="modalIsShow"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    fullscreen
+    @close="closeHandle"
+  >
+    <div slot="title">{{ rowData.outlineName }}</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" :row-data="rowData"></component>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import CourseOutlineBase from "./CourseOutlineBase.vue";
+import CourseOutlineTarget from "./CourseOutlineTarget.vue";
+// import CourseOutlineKnowledge from "./CourseOutlineKnowledge.vue";
+import { mapActions } from "vuex";
+
+export default {
+  name: "detail-course-outline",
+  components: {
+    CourseOutlineBase,
+    CourseOutlineTarget,
+    // CourseOutlineKnowledge,
+  },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      curTab: "CourseOutlineBase",
+      tabs: [
+        {
+          name: "基本信息",
+          val: "CourseOutlineBase",
+        },
+        {
+          name: "课程目标",
+          val: "CourseOutlineTarget",
+        },
+        // {
+        //   name: "知识点",
+        //   val: "CourseOutlineKnowledge",
+        // },
+      ],
+    };
+  },
+  methods: {
+    ...mapActions("target", ["updateCwStatus"]),
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    selectMenu(tab) {
+      this.curTab = tab;
+      if (tab === "CourseOutlineTarget") {
+        this.updateCwStatus({
+          obeCourseOutlineId: this.rowData.id,
+        });
+      }
+    },
+    closeHandle() {
+      this.curTab = "CourseOutlineBase";
+    },
+  },
+};
+</script>

+ 233 - 0
src/modules/target/components/course-outline/ModifyCourseOutline.vue

@@ -0,0 +1,233 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    :title="title"
+    top="10vh"
+    width="600px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      label-width="120px"
+    >
+      <el-form-item prop="courseOutlineName" label="课程大纲名称:">
+        <el-input
+          v-model.trim="modalForm.courseOutlineName"
+          placeholder="课程大纲名称"
+          clearable
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="cultureProgramId" label="所属培养方案:">
+        <el-select
+          v-model="modalForm.cultureProgramId"
+          placeholder="请选择"
+          filterable
+          clearable
+          class="width-full"
+          @change="trainingPlanChange"
+        >
+          <el-option
+            v-for="item in trainingPlans"
+            :key="item.id"
+            :value="item.id"
+            :label="item.name"
+          >
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="semesterId" label="修读学期:">
+        <semester-select
+          v-model="modalForm.semesterId"
+          placeholder="修读学期"
+          class="width-full"
+        ></semester-select>
+      </el-form-item>
+      <el-form-item prop="courseId" label="课程:">
+        <el-select
+          v-model="modalForm.courseId"
+          placeholder="请选择"
+          filterable
+          clearable
+          class="width-full"
+        >
+          <el-option
+            v-for="item in courses"
+            :key="item.id"
+            :value="item.id"
+            :label="item.name"
+          >
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="evaluationMode" label="考核方式:">
+        <el-select
+          v-model="modalForm.evaluationMode"
+          placeholder="请选择"
+          clearable
+          style="width: 240px"
+        >
+          <el-option
+            v-for="(val, key) in EVALUATION_MODE"
+            :key="key"
+            :value="key"
+            :label="val"
+          >
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="courseType" label="课程类别:">
+        <el-select
+          v-model="modalForm.courseType"
+          placeholder="请选择"
+          clearable
+          style="width: 240px"
+        >
+          <el-option
+            v-for="(val, key) in COURSE_TYPE"
+            :key="key"
+            :value="key"
+            :label="val"
+          >
+          </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 {
+  updateCourseOutline,
+  selectableTrainingPlanList,
+  selectableTrainingPlanCourseList,
+} from "../../api";
+
+import { EVALUATION_MODE, COURSE_TYPE } from "@/constants/enumerate";
+
+const initModalForm = {
+  id: null,
+  courseOutlineName: "",
+  courseId: "",
+  evaluationMode: "",
+  courseType: "",
+  cultureProgramId: "",
+  semesterId: "",
+};
+
+export default {
+  name: "modify-course-outline",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      EVALUATION_MODE,
+      COURSE_TYPE,
+      modalForm: { ...initModalForm },
+      courses: [],
+      trainingPlans: [],
+      rules: {
+        courseOutlineName: [
+          { required: true, message: "请输入课程大纲名称", trigger: "change" },
+          {
+            message: "课程大纲名称不能超过30个字",
+            max: 30,
+            trigger: "change",
+          },
+        ],
+        courseId: [
+          { required: true, message: "请选择课程", trigger: "change" },
+        ],
+        evaluationMode: [
+          { required: true, message: "请选择考核方式", trigger: "change" },
+        ],
+        courseType: [
+          { required: true, message: "请选择课程类别", trigger: "change" },
+        ],
+        semesterId: [
+          { required: true, message: "请选择修读学期", trigger: "change" },
+        ],
+        cultureProgramId: [
+          { required: true, message: "请选择所属培养方案", trigger: "change" },
+        ],
+      },
+    };
+  },
+  computed: {
+    isEdit() {
+      return !!this.instance.id;
+    },
+    title() {
+      return (this.isEdit ? "编辑" : "新增") + "课程大纲";
+    },
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = this.$objAssign(initModalForm, this.instance);
+      this.modalForm.courseOutlineName = this.instance.outlineName || "";
+      this.getTrainingPlans();
+      this.getCourses();
+
+      this.$nextTick(() => {
+        this.$refs.modalFormComp.clearValidate();
+      });
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async getTrainingPlans() {
+      this.trainingPlans = [];
+      const res = await selectableTrainingPlanList();
+      this.trainingPlans = res || [];
+    },
+    async getCourses() {
+      this.courses = [];
+      if (!this.modalForm.cultureProgramId) return;
+      const res = await selectableTrainingPlanCourseList(
+        this.modalForm.cultureProgramId
+      );
+      this.courses = res || [];
+    },
+    trainingPlanChange() {
+      this.modalForm.courseId = "";
+      this.getCourses();
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const data = await updateCourseOutline(this.modalForm).catch(() => {});
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 131 - 0
src/modules/target/components/course-outline/ModifyCourseOutlineKnowledge.vue

@@ -0,0 +1,131 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    :title="title"
+    top="10vh"
+    width="600px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :key="modalForm.id"
+      :rules="rules"
+      label-width="120px"
+    >
+      <el-form-item label="所属知识点:">
+        <el-select
+          v-model="modalForm.parentId"
+          placeholder="请选择"
+          filterable
+          clearable
+        >
+          <el-option
+            v-for="item in mainOptions"
+            :key="item.id"
+            :value="item.id"
+            :label="item.name"
+          >
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="name" label="知识点名称:">
+        <el-input
+          v-model.trim="modalForm.name"
+          placeholder="知识点名称"
+          clearable
+        ></el-input>
+      </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 { updateCourseOutlineKnowledge } from "../../api";
+
+const initModalForm = {
+  id: null,
+  parentId: "",
+  name: "",
+};
+
+export default {
+  name: "modify-course-outline-knowledge",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    mainOptions: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      rules: {
+        name: [
+          { required: true, message: "请输入知识点名称", trigger: "change" },
+          {
+            message: "知识点名称不能超过30个字",
+            max: 30,
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  computed: {
+    isEdit() {
+      return !!this.instance.id;
+    },
+    title() {
+      return (this.isEdit ? "编辑" : "新增") + "知识点";
+    },
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = this.$objAssign(initModalForm, this.instance);
+    },
+    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 data = await updateCourseOutlineKnowledge(this.modalForm).catch(
+        () => {}
+      );
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 271 - 0
src/modules/target/components/course-outline/ModifyCourseOutlineTarget.vue

@@ -0,0 +1,271 @@
+<template>
+  <div>
+    <el-dialog
+      class="modify-course-outline-target"
+      :visible.sync="modalIsShow"
+      :title="title"
+      top="10vh"
+      width="600px"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      append-to-body
+      @open="visibleChange"
+    >
+      <el-form
+        ref="modalFormComp"
+        :model="modalForm"
+        :rules="rules"
+        :key="modalForm.id"
+        label-width="120px"
+      >
+        <el-form-item prop="targetName" label="课程目标名称:">
+          <el-input
+            v-model.trim="modalForm.targetName"
+            placeholder="请输入课程目标名称"
+            clearable
+          ></el-input>
+        </el-form-item>
+        <el-form-item
+          prop="obeCultureProgramRequirementId"
+          label="毕业要求指标点:"
+        >
+          <requirement-select
+            v-if="modalIsShow"
+            v-model="modalForm.obeCultureProgramRequirementId"
+            :tree-data="requirementList"
+            @change="requirementChange"
+          ></requirement-select>
+
+          <el-button
+            type="text"
+            class="btn-act-primary ml-1"
+            icon="el-icon-circle-plus-outline"
+            @click="toSelectDimension"
+            >选择知识点</el-button
+          >
+        </el-form-item>
+        <el-form-item prop="degreeRequirement" label="目标分解详情:">
+          <el-input
+            v-model="modalForm.degreeRequirement"
+            placeholder="请输入目标分解详情"
+            type="textarea"
+            :autosize="{ minRows: 2, maxRows: 6 }"
+            :maxlength="999"
+            resize="none"
+            show-word-limit
+          ></el-input>
+        </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>
+
+    <!-- SelectDimensionDialog -->
+    <select-dimension-dialog
+      ref="SelectDimensionDialog"
+      :param="{ obeCourseOutlineId: instance.obeCourseOutlineId }"
+      :selected-data="modalForm.dimensionIdList"
+      :disabled-data="disabledDimensionIds"
+      @confirm="dimensionSelected"
+      @enforce-close="enforceClose"
+    ></select-dimension-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  updateCourseOutlineTarget,
+  courseOutlineTargetRequirementKnowledge,
+} from "../../api";
+import RequirementSelect from "./RequirementSelect.vue";
+import SelectDimensionDialog from "./SelectDimensionDialog.vue";
+import { mapState } from "vuex";
+
+const initModalForm = {
+  id: null,
+  obeCourseOutlineId: "",
+  targetName: "",
+  degreeRequirement: "",
+  obeCultureProgramRequirementId: "",
+  dimensionIdList: [],
+};
+
+export default {
+  name: "modify-course-outline-target",
+  components: { SelectDimensionDialog, RequirementSelect },
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    dimensions: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    requirementList: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+  },
+  computed: {
+    ...mapState("target", ["cwStatus"]),
+    isEdit() {
+      return !!this.instance.id;
+    },
+    title() {
+      return (this.isEdit ? "编辑" : "新增") + "课程目标";
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      dimensionList: [],
+      disabledDimensionIds: [],
+      rules: {
+        targetName: [
+          {
+            required: true,
+            message: "请输入课程目标名称",
+            trigger: "change",
+          },
+          {
+            max: 30,
+            message: "课程目标名称不能超过30个字",
+            trigger: "change",
+          },
+        ],
+        obeCultureProgramRequirementId: [
+          {
+            validator: (rule, value, callback) => {
+              if (!value) {
+                return callback(new Error("请选择毕业要求指标点"));
+              }
+
+              if (!this.modalForm.dimensionIdList.length) {
+                return callback(new Error("请选择知识点"));
+              }
+
+              return callback();
+            },
+          },
+        ],
+        degreeRequirement: [
+          {
+            required: false,
+            max: 999,
+            message: "目标分解详情不能超过999个字",
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  methods: {
+    initData(val) {
+      this.modalForm = this.$objAssign(initModalForm, val);
+      if (val.dimensionList) {
+        this.dimensionList = [...val.dimensionList];
+        this.updateDimensionIds();
+      } else {
+        this.dimensionList = [];
+        this.modalForm.dimensionIdList = [];
+      }
+      this.updateDisabledDimensionIds(
+        this.modalForm.obeCultureProgramRequirementId
+      );
+    },
+    visibleChange() {
+      this.initData(this.instance);
+    },
+    enforceClose() {
+      this.modalIsShow = false;
+      this.$emit("enforce-close");
+    },
+    async requirementChange() {
+      this.modalForm.dimensionIdList = [];
+      if (!this.modalForm.obeCultureProgramRequirementId) return;
+      const res = await courseOutlineTargetRequirementKnowledge({
+        obeCourseOutlineId: this.instance.obeCourseOutlineId,
+        obeCultureProgramRequirementId:
+          this.modalForm.obeCultureProgramRequirementId,
+      });
+
+      this.modalForm.dimensionIdList = (res || []).map((item) => item.id);
+      this.$refs.modalFormComp.validateField("obeCultureProgramRequirementId");
+      this.updateDisabledDimensionIds(
+        this.modalForm.obeCultureProgramRequirementId
+      );
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    toSelectDimension() {
+      if (!this.modalForm.obeCultureProgramRequirementId) {
+        this.$message.error("请选择毕业要求指标点");
+        return;
+      }
+
+      this.$refs.SelectDimensionDialog.open();
+    },
+    updateDimensionIds() {
+      this.modalForm.dimensionIdList = this.dimensionList.map(
+        (item) => item.id
+      );
+    },
+    updateDisabledDimensionIds(obeCultureProgramRequirementId) {
+      this.disabledDimensionIds = this.dimensions
+        .filter((item) => item.requirementId !== obeCultureProgramRequirementId)
+        .map((item) => item.dimensionIds)
+        .flat();
+    },
+    dimensionSelected(dimensions) {
+      this.dimensionList = [...dimensions];
+      this.updateDimensionIds();
+      this.$refs.modalFormComp.validateField("obeCultureProgramRequirementId");
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (this.cwStatus.everSettingStatus) {
+        const confirm = await this.$confirm(
+          `${this.title}会影响权重设置,确定要${this.title}吗?`,
+          "提示",
+          {
+            type: "warning",
+          }
+        ).catch(() => {});
+        if (confirm !== "confirm") return;
+      }
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const res = await updateCourseOutlineTarget(this.modalForm).catch(
+        () => {}
+      );
+      this.isSubmit = false;
+
+      if (!res) return;
+
+      this.$message.success(this.title + "成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 99 - 0
src/modules/target/components/course-outline/ModifyCourseOutlineTargetPredict.vue

@@ -0,0 +1,99 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    title="课程目标预期值"
+    top="10vh"
+    width="300px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form ref="modalFormComp" :model="modalForm" :rules="rules">
+      <el-form-item prop="expectValue" label="预期值:">
+        <el-input-number
+          v-model="modalForm.expectValue"
+          :min="0.01"
+          :max="1"
+          :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 { updateCourseOutlineTargetPredict } from "../../api";
+
+const initModalForm = {
+  obeCourseOutlineId: null,
+  expectValue: "",
+};
+
+export default {
+  name: "modify-course-outline-target-predict",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      rules: {
+        expectValue: [
+          {
+            required: true,
+            message: "请输入课程目标预期值",
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = {
+        obeCourseOutlineId: this.rowData.id,
+        expectValue: this.rowData.expectValue,
+      };
+    },
+    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 data = await updateCourseOutlineTargetPredict(this.modalForm).catch(
+        () => {}
+      );
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 293 - 0
src/modules/target/components/course-outline/RequirementSelect.vue

@@ -0,0 +1,293 @@
+<template>
+  <div
+    class="org-select el-select major-select el-select--small"
+    @click="switchOpen"
+  >
+    <div v-if="multiple" class="el-select__tags">
+      <el-tag
+        v-for="item in selectedList"
+        :key="item.id"
+        @close="deleteTag($event, item)"
+        >{{ item.name }}</el-tag
+      >
+    </div>
+    <div
+      :class="[
+        'el-input el-input--small el-input--suffix',
+        { 'is-focus': isFocus },
+      ]"
+      @mouseenter="inputHovering = true"
+      @mouseleave="inputHovering = false"
+    >
+      <input
+        ref="inputRef"
+        type="text"
+        autocomplete="off"
+        :placeholder="placeholder"
+        class="el-input__inner"
+        :value="selectedOrg.name"
+        readonly
+      />
+      <span class="el-input__suffix">
+        <span class="el-input__suffix-inner">
+          <i
+            v-show="!showClose"
+            :class="[
+              'el-select__caret',
+              'el-input__icon',
+              'el-icon-arrow-up',
+              { 'is-reverse': visible },
+            ]"
+          ></i>
+          <i
+            v-if="showClose"
+            class="el-select__caret el-input__icon el-icon-circle-close"
+            @click="handleClearClick"
+          ></i>
+        </span>
+      </span>
+    </div>
+
+    <el-popover
+      popper-class="org-popover"
+      placement="bottom-start"
+      trigger="manual"
+      v-model="visible"
+    >
+      <div ref="popoverRef" slot="reference"></div>
+      <div class="org-popover-tree" v-clickoutside="handleClose">
+        <el-tree
+          ref="OrgTree"
+          :data="orgs"
+          default-expand-all
+          node-key="id"
+          :props="defaultProps"
+          check-on-click-node
+          :expand-on-click-node="false"
+          @node-click="nodeClick"
+        >
+          <span
+            slot-scope="{ node, data }"
+            :class="[
+              'custom-tree-node',
+              {
+                'is-select': checkSelected(data),
+                'is-disabled': data.disabled,
+              },
+            ]"
+          >
+            <span>{{ node.label }}</span>
+            <span>
+              <i class="el-icon-check"></i>
+            </span>
+          </span>
+        </el-tree>
+      </div>
+    </el-popover>
+  </div>
+</template>
+
+<script>
+import { courseOutlineTargetRequirementList } from "../../api";
+import Clickoutside from "element-ui/src/utils/clickoutside";
+
+export default {
+  name: "requirement-select",
+  props: {
+    value: {
+      type: [Array, String],
+    },
+    placeholder: { type: String, default: "请选择" },
+    multiple: {
+      type: Boolean,
+      default: false,
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+    clearable: {
+      type: Boolean,
+      default: true,
+    },
+    filterParam: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+    treeData: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+  },
+  directives: { Clickoutside },
+  data() {
+    return {
+      selectedValue: "",
+      selectedOrg: {},
+      selectedOrgList: [],
+      selectedOrgIds: [],
+      visible: false,
+      isFocus: false,
+      inputHovering: false,
+      orgs: [],
+      defaultProps: {
+        label: "name",
+        disabled: "disabled",
+      },
+    };
+  },
+  computed: {
+    showClose() {
+      let hasValue = this.multiple
+        ? Array.isArray(this.value) && this.value.length > 0
+        : this.value !== undefined && this.value !== null && this.value !== "";
+      let criteria = this.clearable && this.inputHovering && hasValue;
+      return criteria;
+    },
+  },
+  watch: {
+    value(val, oldval) {
+      if (val !== oldval && this.selectedValue !== val) this.initSelected(val);
+    },
+  },
+  async mounted() {
+    await this.getList();
+    if (this.value) this.initSelected(this.value);
+  },
+  methods: {
+    async getList() {
+      if (this.treeData) {
+        this.orgs = this.treeData;
+        return;
+      }
+
+      const data = await courseOutlineTargetRequirementList(this.filterParam);
+      this.orgs = (data || []).map((item) => {
+        item.children = item.subRequirements.map((elem) => {
+          const nelem = { ...elem };
+          nelem.parentName = item.name;
+          return nelem;
+        });
+        return item;
+      });
+    },
+    switchOpen() {
+      if (this.visible) {
+        this.handleClose();
+      } else {
+        this.handleOpen();
+      }
+    },
+    handleOpen() {
+      this.isFocus = true;
+      setTimeout(() => {
+        this.visible = true;
+      }, 200);
+    },
+    handleClose() {
+      this.visible = false;
+      this.isFocus = false;
+    },
+    getSelectedData(selectedIds) {
+      let selectedData = [];
+      if (!selectedIds.length) return [];
+
+      const findTree = (list) => {
+        list.forEach((item) => {
+          if (selectedIds.includes(item.id)) {
+            selectedData.push({ ...item });
+          }
+          if (item.children && item.children.length) {
+            findTree(item.children);
+          }
+        });
+      };
+
+      findTree(this.orgs);
+
+      return selectedData;
+    },
+    initSelected(val) {
+      if (!this.orgs.length) return;
+      if (this.multiple) {
+        const selectedIds = val || [];
+        const selectedData = this.getSelectedData(selectedIds);
+        this.selectedOrg = {};
+        this.selectedOrgList = selectedData;
+      } else {
+        const selectedIds = val ? [val] : [];
+        const selectedData = this.getSelectedData(selectedIds);
+        if (selectedData.length) {
+          this.selectedOrg = { ...selectedData[0] };
+          this.selectedOrgList = selectedData;
+        } else {
+          this.selectedOrg = {};
+          this.selectedOrgList = [];
+        }
+      }
+      this.updateSelectOrgIds();
+      this.emitChange();
+    },
+    nodeClick(data) {
+      if (data.children || data.disabled) return;
+
+      if (!this.multiple) {
+        this.selectedOrg = { ...data };
+        this.selectedOrgList = [{ ...data }];
+        this.updateSelectOrgIds();
+        this.visible = false;
+        this.emitChange();
+        return;
+      }
+
+      if (this.selectedOrgIds.includes(data.id)) {
+        this.selectedOrgList = this.selectedOrgList.filter(
+          (item) => item.id !== data.id
+        );
+      } else {
+        this.selectedOrgList.push({ ...data });
+      }
+      this.updateSelectOrgIds();
+      this.emitChange();
+    },
+    updateSelectOrgIds() {
+      this.selectedOrgIds = this.selectedOrgList.map((item) => item.id);
+    },
+    checkSelected(data) {
+      return this.selectedOrgIds.includes(data.id);
+    },
+    deleteTag(event, data) {
+      if (this.selectedOrgIds.includes(data.id)) {
+        this.selectedOrgList = this.selectedOrgList.filter(
+          (item) => item.id !== data.id
+        );
+        this.updateSelectOrgIds();
+        this.emitChange();
+      }
+      event.stopPropagation();
+    },
+    handleClearClick(event) {
+      event.stopPropagation();
+      this.selectedOrgList = [];
+      this.selectedOrg = {};
+      this.updateSelectOrgIds();
+      this.emitChange();
+      this.handleClose();
+    },
+    emitChange() {
+      this.selectedValue = this.multiple
+        ? this.selectedOrgIds
+        : this.selectedOrgIds[0];
+      this.$emit("input", this.selectedValue);
+      this.$emit(
+        "change",
+        this.multiple ? this.selectedOrgList : this.selectedOrgList[0]
+      );
+    },
+  },
+};
+</script>

+ 187 - 0
src/modules/target/components/course-outline/SelectDimensionDialog.vue

@@ -0,0 +1,187 @@
+<template>
+  <div>
+    <el-dialog
+      class="select-dimension-dialog page-dialog"
+      :visible.sync="modalIsShow"
+      title="选择知识点"
+      top="10px"
+      width="600px"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      append-to-body
+      @opened="visibleChange"
+    >
+      <div class="box-justify mb-2">
+        <div></div>
+        <el-button type="success" @click="toImport">导入课程知识点</el-button>
+      </div>
+      <div class="part-box part-box-pad mb-0">
+        <el-tree
+          ref="treeRef"
+          :key="treeKey"
+          :data="treeData"
+          show-checkbox
+          check-on-click-node
+          check-strictly
+          :expand-on-click-node="false"
+          node-key="id"
+          default-expand-all
+          :props="defaultProps"
+        >
+        </el-tree>
+      </div>
+
+      <div slot="footer">
+        <el-button type="primary" @click="submit">确认</el-button>
+        <el-button @click="cancel">取消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- ImportFile -->
+    <import-file
+      ref="ImportFile"
+      title="导入课程知识点"
+      :upload-url="uploadUrl"
+      :upload-data="{
+        obeCourseOutlineId: param.obeCourseOutlineId,
+      }"
+      :format="['xls', 'xlsx']"
+      :download-handle="() => downloadTemplate('courseProperty')"
+      :download-filename="dfilename"
+      :auto-upload="false"
+      @upload-success="dimensionUploaded"
+    ></import-file>
+  </div>
+</template>
+
+<script>
+import { courseOutlineTargetKnowledgeList } from "../../api";
+import ImportFile from "@/components/ImportFile.vue";
+import templateDownload from "@/mixins/templateDownload";
+
+export default {
+  name: "select-dimension-dialog",
+  components: { ImportFile },
+  mixins: [templateDownload],
+  props: {
+    selectedData: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    disabledData: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    param: {
+      type: Object,
+      default() {
+        return {
+          obeCourseOutlineId: "",
+        };
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      treeData: [],
+      defaultProps: {
+        children: "children",
+        label: "name",
+      },
+      treeKey: "",
+      // import
+      uploadUrl: "/api/admin/obe/course_target/dimension_import",
+      dfilename: "课程知识点导入模板.xlsx",
+    };
+  },
+  watch: {
+    "param.obeCourseOutlineId": {
+      immediate: true,
+      handler(val, oldVal) {
+        if (val !== oldVal) {
+          this.getTree();
+        }
+      },
+    },
+  },
+  methods: {
+    async dimensionUploaded() {
+      const data = await courseOutlineTargetKnowledgeList({
+        obeCourseOutlineId: this.param.obeCourseOutlineId,
+      });
+      this.treeData = data || [];
+
+      // 已存在课程目标的情况,强制关闭弹窗
+      if (this.disabledData.length) {
+        this.modalIsShow = false;
+        this.$emit("enforce-close");
+        return;
+      }
+
+      this.$nextTick(() => {
+        this.$refs.treeRef.setCheckedKeys([]);
+      });
+    },
+    async getTree() {
+      if (!this.param.obeCourseOutlineId) return;
+
+      const data = await courseOutlineTargetKnowledgeList({
+        obeCourseOutlineId: this.param.obeCourseOutlineId,
+      });
+      this.treeData = data || [];
+      this.updateDisableNode();
+    },
+    updateDisableNode() {
+      const mdTree = (list) => {
+        list.forEach((item) => {
+          item.disabled = this.disabledData.includes(item.id);
+          if (item.children && item.children.length) mdTree(item.children);
+        });
+      };
+      mdTree(this.treeData);
+      this.treeKey = this.$randomCode();
+    },
+    toImport() {
+      this.$refs.ImportFile.open();
+    },
+    visibleChange() {
+      this.updateDisableNode();
+
+      this.$nextTick(() => {
+        this.$refs.treeRef.setCheckedKeys(this.selectedData);
+      });
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    submit() {
+      const selectedNodes = this.$refs.treeRef.getCheckedNodes();
+
+      if (!selectedNodes.length) {
+        this.$message.error("请选择知识点");
+        return;
+      }
+
+      this.$emit(
+        "confirm",
+        selectedNodes.map((item) => {
+          return {
+            id: item.id,
+            name: item.name,
+            code: item.code,
+          };
+        })
+      );
+      this.cancel();
+    },
+  },
+};
+</script>

+ 80 - 0
src/modules/target/components/requirement-statistics/DetailRequirementStatistics.vue

@@ -0,0 +1,80 @@
+<template>
+  <el-dialog
+    class="page-dialog"
+    :visible.sync="modalIsShow"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    fullscreen
+    @close="closeHandle"
+  >
+    <div slot="title">{{ rowData.obeCultureProgramName }}</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" :row-data="rowData"></component>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import RequirementStatisticsRadar from "./RequirementStatisticsRadar.vue";
+import RequirementStatisticsStatement from "./RequirementStatisticsStatement.vue";
+
+export default {
+  name: "detail-requirement-statistics",
+  components: {
+    RequirementStatisticsRadar,
+    RequirementStatisticsStatement,
+  },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      curTab: "RequirementStatisticsRadar",
+      tabs: [
+        {
+          name: "毕业要求达成度雷达图",
+          val: "RequirementStatisticsRadar",
+        },
+        {
+          name: "毕业要求达成情况详细报表",
+          val: "RequirementStatisticsStatement",
+        },
+      ],
+    };
+  },
+  methods: {
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    selectMenu(tab) {
+      this.curTab = tab;
+    },
+    closeHandle() {
+      this.curTab = "RequirementStatisticsRadar";
+    },
+  },
+};
+</script>

+ 157 - 0
src/modules/target/components/requirement-statistics/RequirementStatisticsRadar.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="part-box part-box-pad">
+    <el-row type="flex" :gutter="20">
+      <el-col :span="12">
+        <div class="chart-box" style="height: 520px">
+          <v-chart
+            v-if="chartOption"
+            :option="chartOption"
+            autoresize
+          ></v-chart>
+        </div>
+      </el-col>
+      <el-col :span="12">
+        <el-table :data="dataList" border>
+          <el-table-column label="毕业要求" min-width="200">
+            <template slot-scope="scope">
+              {{ scope.$index + 1 }}.{{ scope.row.name }}
+            </template>
+          </el-table-column>
+          <el-table-column label="预期值" prop="expectValue"></el-table-column>
+          <el-table-column label="实际值" prop="matrixDegree">
+            <template slot-scope="scope">
+              {{ scope.row.matrixDegree | defaultFieldFilter }}
+            </template>
+          </el-table-column>
+          <el-table-column label="评价结果">
+            <span
+              slot-scope="scope"
+              :class="
+                scope.row.matrixDegree >= scope.row.expectValue
+                  ? 'color-success'
+                  : 'color-gray'
+              "
+            >
+              {{
+                scope.row.matrixDegree >= scope.row.expectValue
+                  ? "达成"
+                  : "未达成"
+              }}
+            </span>
+          </el-table-column>
+          <el-table-column class-name="action-column" label="操作" width="100">
+            <template slot-scope="scope">
+              <el-popover placement="right" trigger="hover">
+                <el-table :data="scope.row.obeSubRequirements" border>
+                  <el-table-column
+                    label="毕业要求指标"
+                    prop="name"
+                    width="120"
+                  ></el-table-column>
+                  <el-table-column
+                    label="达成值"
+                    prop="matrixDegree"
+                    width="100"
+                  >
+                    <template slot-scope="scope">
+                      {{ scope.row.matrixDegree | defaultFieldFilter }}
+                    </template>
+                  </el-table-column>
+                </el-table>
+                <el-button slot="reference" class="btn-act-primary" type="text"
+                  >查看详情</el-button
+                >
+              </el-popover>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { maxNum } from "@/plugins/utils";
+import { requirementStatisticsRadar } from "../../api";
+
+export default {
+  name: "requirement-statistics-radar",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      dataList: [],
+      chartOption: null,
+    };
+  },
+  mounted() {
+    this.getDetail();
+  },
+  methods: {
+    async getDetail() {
+      const res = await requirementStatisticsRadar({
+        cultureProgramId: this.rowData.cultureProgramId,
+      });
+      this.dataList = res.obeRequirements || [];
+      this.updateChartOption();
+    },
+    updateChartOption() {
+      const vals = this.dataList
+        .map((item) => [item.expectValue, item.matrixDegree])
+        .flat();
+      const maxVal = Math.min(maxNum(vals) * 1.2, 1);
+      const option = {
+        color: ["#3a5ae5", "#fe5d4e"],
+        legend: {
+          top: 0,
+          data: ["预期值", "实际值"],
+          itemWidth: 16,
+          itemHeight: 4,
+          itemGap: 22,
+          left: "center",
+        },
+        radar: {
+          shape: "circle",
+          axisName: {
+            color: "#1f2230",
+            fontSize: 14,
+          },
+          axisLine: {
+            lineStyle: {
+              color: "#d3d5e0",
+            },
+          },
+          indicator: this.dataList.map((item, index) => {
+            return {
+              name: `${index + 1}.${item.name}`,
+              max: maxVal,
+            };
+          }),
+        },
+        series: [
+          {
+            type: "radar",
+            data: [
+              {
+                value: this.dataList.map((item) => item.expectValue),
+                name: "预期值",
+              },
+              {
+                value: this.dataList.map((item) => item.matrixDegree),
+                name: "实际值",
+              },
+            ],
+          },
+        ],
+      };
+      this.chartOption = option;
+    },
+  },
+};
+</script>

+ 240 - 0
src/modules/target/components/requirement-statistics/RequirementStatisticsStatement.vue

@@ -0,0 +1,240 @@
+<template>
+  <div class="part-box part-box-pad">
+    <el-table
+      :data="dataList"
+      border
+      :span-method="spanMethod"
+      :max-height="600"
+    >
+      <el-table-column label="课程信息" align="center" fixed="left" width="500">
+        <el-table-column
+          label="课程名称(代码)"
+          width="240"
+          show-overflow-tooltip
+        >
+          <template slot-scope="scope">
+            <span>
+              {{ scope.row.courseName }}
+            </span>
+            <span v-if="scope.row.courseCode">
+              ({{ scope.row.courseCode }})
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="学期"
+          prop="semesterName"
+          width="200"
+        ></el-table-column>
+        <el-table-column
+          label="学分"
+          prop="credit"
+          width="60"
+          align="center"
+        ></el-table-column>
+      </el-table-column>
+      <el-table-column label="毕业要求" align="center">
+        <template v-if="hasSubRequirements">
+          <el-table-column
+            v-for="(column, cindex) in columns"
+            :key="cindex"
+            :label="column.name"
+            align="center"
+          >
+            <el-table-column
+              v-for="(subr, sindex) in column.subRequirements"
+              :key="sindex"
+              align="center"
+              min-width="75"
+            >
+              <template slot="header">
+                <span v-if="subr.name === 'null'"></span>
+                <template v-else>
+                  <span>{{ subr.name }}</span>
+                  <el-tooltip v-if="subr.content" effect="dark" placement="top">
+                    <div slot="content" class="tooltip-area">
+                      {{ subr.content }}
+                    </div>
+                    <i class="el-icon-info ml-1 tooltip-info-icon"></i>
+                  </el-tooltip>
+                </template>
+              </template>
+              <template slot-scope="scope">
+                {{
+                  scope.row.obeRequirements[cindex].obeSubRequirements[sindex]
+                    .matrixDegree | defaultFieldFilter
+                }}
+              </template>
+            </el-table-column>
+          </el-table-column>
+        </template>
+
+        <template v-else>
+          <el-table-column
+            v-for="(column, cindex) in columns"
+            :key="cindex"
+            :label="column.name"
+            align="center"
+            min-width="60"
+          >
+            <template slot-scope="scope">
+              {{
+                scope.row.obeRequirements[cindex].matrixDegree
+                  | defaultFieldFilter
+              }}
+            </template>
+          </el-table-column>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import { calcSum, minNum, toPrecision, listFilterEmpty } from "@/plugins/utils";
+import { requirementStatisticsDetail } from "../../api";
+
+export default {
+  name: "requirement-statistics-statement",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      dataList: [],
+      columns: [],
+      hasSubRequirements: false,
+      rowCount: 0,
+      totalRowColMap: {},
+    };
+  },
+  mounted() {
+    this.getList();
+  },
+  methods: {
+    async getList() {
+      const res = await requirementStatisticsDetail({
+        cultureProgramId: this.rowData.cultureProgramId,
+      });
+      this.dataList = res || [];
+      if (this.dataList[0]) this.parseColumns(this.dataList[0].obeRequirements);
+      this.updateTotalInfo();
+      this.rowCount = this.dataList.length;
+    },
+    parseColumns(requirements) {
+      this.hasSubRequirements = requirements.some(
+        (item) => item.obeSubRequirements[0].name !== null
+      );
+      if (!this.hasSubRequirements) {
+        this.columns = requirements.map((item) => {
+          return { name: item.name };
+        });
+        return;
+      }
+
+      this.columns = requirements.map((item) => {
+        return {
+          name: item.name,
+          subRequirements: item.obeSubRequirements.map((subr) => {
+            return {
+              name: subr.name,
+              content: subr.content,
+            };
+          }),
+        };
+      });
+    },
+    updateTotalInfo() {
+      const totalRowColMap = {};
+      let columnIndex = 3;
+      const totalObeRequirements = this.columns.map((column, cindex) => {
+        const obeSubRequirements = column.subRequirements.map(
+          (subr, sindex) => {
+            const dList = listFilterEmpty(
+              this.dataList.map(
+                (item) =>
+                  item.obeRequirements[cindex].obeSubRequirements[sindex]
+                    .matrixDegree
+              )
+            );
+            const matrixDegree = dList.length
+              ? toPrecision(calcSum(dList), 2)
+              : null;
+
+            return {
+              name: subr.name,
+              matrixDegree,
+            };
+          }
+        );
+        totalRowColMap[columnIndex] = column.subRequirements.length;
+        columnIndex += column.subRequirements.length;
+
+        const validVals = listFilterEmpty(
+          obeSubRequirements.map((subr) => subr.matrixDegree)
+        );
+        const minMatrixDegree = validVals.length ? minNum(validVals) : null;
+
+        return {
+          name: column.name,
+          obeSubRequirements,
+          matrixDegree: minMatrixDegree,
+        };
+      });
+      this.totalRowColMap = totalRowColMap;
+
+      const totalRequirementColumn = {
+        courseName: "毕业要求指标达成度",
+        semesterName: "",
+        credit: "",
+        obeRequirements: totalObeRequirements,
+      };
+      this.dataList.push(totalRequirementColumn);
+
+      const totalColumn = {
+        courseName: "毕业要求达成度",
+        semesterName: "",
+        credit: "",
+        obeRequirements: totalObeRequirements.map((item) => {
+          const nitem = { ...item };
+          nitem.obeSubRequirements = item.obeSubRequirements.map(
+            (elem, eindex) => {
+              const nelem = { ...elem };
+              nelem.matrixDegree = item.matrixDegree;
+              return nelem;
+            }
+          );
+          return nitem;
+        }),
+      };
+      this.dataList.push(totalColumn);
+    },
+    spanMethod({ rowIndex, columnIndex, column }) {
+      if (rowIndex === this.rowCount - 2) {
+        if (columnIndex === 0) {
+          return { rowspan: 1, colspan: 3 };
+        } else if (columnIndex <= 2) {
+          return [0, 0];
+        }
+      }
+
+      if (rowIndex === this.rowCount - 1) {
+        if (columnIndex === 0) {
+          return { rowspan: 1, colspan: 3 };
+        } else if (columnIndex <= 2) {
+          return [0, 0];
+        } else {
+          return this.totalRowColMap[columnIndex]
+            ? [1, this.totalRowColMap[columnIndex]]
+            : [0, 0];
+        }
+      }
+    },
+  },
+};
+</script>

+ 409 - 0
src/modules/target/components/student-target/DetailStudentTarget.vue

@@ -0,0 +1,409 @@
+<template>
+  <el-dialog
+    class="page-dialog"
+    :visible.sync="modalIsShow"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    fullscreen
+    @opened="openedHandle"
+  >
+    <div slot="title">{{ rowData.studentName }}毕业要求达成情况</div>
+
+    <div class="part-box part-box-pad">
+      <div class="part-box-head">
+        <h3>个人与本院对比情况</h3>
+      </div>
+      <div class="chart-box" style="height: 360px">
+        <v-chart v-if="chartOption" :option="chartOption" autoresize></v-chart>
+      </div>
+    </div>
+
+    <div class="part-box part-box-pad">
+      <div class="part-box-head">
+        <h3>个人毕业要求达成度情况</h3>
+      </div>
+
+      <el-form ref="FilterForm" label-position="left" inline>
+        <el-form-item label-width="0px">
+          <semester-select
+            v-model="filter.semesterId"
+            :style="{ width: '220px' }"
+          ></semester-select>
+        </el-form-item>
+        <el-form-item label-width="0px">
+          <el-button type="primary" @click="search">查询</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table
+        ref="TableList"
+        :data="dataList"
+        border
+        :span-method="spanMethod"
+        :max-height="600"
+      >
+        <el-table-column
+          label="课程名称(代码)"
+          width="240"
+          show-overflow-tooltip
+          fixed="left"
+        >
+          <template slot-scope="scope">
+            <span>
+              {{ scope.row.courseName }}
+            </span>
+            <span v-if="scope.row.courseCode">
+              ({{ scope.row.courseCode }})
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="semesterName"
+          label="所属学期"
+          width="200"
+          fixed="left"
+        >
+        </el-table-column>
+        <el-table-column
+          prop="score"
+          label="综合成绩"
+          width="85"
+          align="center"
+          fixed="left"
+        >
+        </el-table-column>
+        <el-table-column label="毕业要求" align="center">
+          <template v-if="hasSubRequirements">
+            <el-table-column
+              v-for="(column, cindex) in columns"
+              :key="cindex"
+              :label="column.name"
+              align="center"
+            >
+              <el-table-column
+                v-for="(subr, sindex) in column.subRequirements"
+                :key="sindex"
+                align="center"
+                min-width="75"
+              >
+                <template slot="header">
+                  <span v-if="subr.name === 'null'"></span>
+                  <template v-else>
+                    <span>{{ subr.name }}</span>
+                    <el-tooltip
+                      v-if="subr.content"
+                      effect="dark"
+                      placement="top"
+                    >
+                      <div slot="content" class="tooltip-area">
+                        {{ subr.content }}
+                      </div>
+                      <i class="el-icon-info ml-1 tooltip-info-icon"></i>
+                    </el-tooltip>
+                  </template>
+                </template>
+                <template slot-scope="scope">
+                  {{
+                    scope.row.requirementDetailList[cindex]
+                      .subRequirementDetailList[sindex].studentDegree
+                      | defaultFieldFilter
+                  }}
+                </template>
+              </el-table-column>
+            </el-table-column>
+          </template>
+
+          <template v-else>
+            <el-table-column
+              v-for="(column, cindex) in columns"
+              :key="cindex"
+              :label="column.name"
+              align="center"
+              min-width="60"
+            >
+              <template slot-scope="scope">
+                {{
+                  scope.row.requirementDetailList[cindex].studentDegree
+                    | defaultFieldFilter
+                }}
+              </template>
+            </el-table-column>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import {
+  splitContent,
+  calcSum,
+  minNum,
+  toPrecision,
+  listFilterEmpty,
+} from "@/plugins/utils";
+import { studentTargetDetail } from "../../api";
+
+export default {
+  name: "detail-requirement-statistics",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      chartData: [],
+      chartOption: null,
+      filter: { semesterId: "" },
+      columns: [],
+      dataList: [],
+      hasSubRequirements: false,
+      rowCount: 0,
+      totalRowColMap: {},
+    };
+  },
+  methods: {
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    openedHandle() {
+      this.search();
+    },
+    async search() {
+      const res = await studentTargetDetail({
+        cultureProgramId: this.rowData.cultureProgramId,
+        studentCode: this.rowData.studentCode,
+        ...this.filter,
+      });
+      this.chartData = res.studentTotalRequirementList || [];
+
+      this.dataList = res.studentCourseRequirementList || [];
+      if (this.dataList[0])
+        this.parseColumns(this.dataList[0].requirementDetailList);
+
+      this.updateTotalInfo();
+      this.rowCount = this.dataList.length;
+
+      this.updateChartOption();
+    },
+    parseColumns(requirements) {
+      this.hasSubRequirements = requirements.some(
+        (item) => item.subRequirementDetailList[0].name !== null
+      );
+      if (!this.hasSubRequirements) {
+        this.columns = requirements.map((item) => {
+          return { name: item.requirementName };
+        });
+        return;
+      }
+
+      this.columns = requirements.map((item) => {
+        return {
+          name: item.requirementName,
+          subRequirements: item.subRequirementDetailList.map((subr) => {
+            return {
+              name: subr.requirementName,
+              content: subr.requirementContent,
+            };
+          }),
+        };
+      });
+    },
+    updateTotalInfo() {
+      const totalRowColMap = {};
+      let columnIndex = 3;
+      const totalObeRequirements = this.columns.map((column, cindex) => {
+        const subRequirementDetailList = column.subRequirements.map(
+          (subr, sindex) => {
+            const dList = listFilterEmpty(
+              this.dataList.map(
+                (item) =>
+                  item.requirementDetailList[cindex].subRequirementDetailList[
+                    sindex
+                  ].studentDegree
+              )
+            );
+            const studentDegree = dList.length
+              ? toPrecision(calcSum(dList), 2)
+              : null;
+
+            return {
+              name: subr.name,
+              studentDegree,
+            };
+          }
+        );
+        totalRowColMap[columnIndex] = column.subRequirements.length;
+        columnIndex += column.subRequirements.length;
+
+        const validVals = listFilterEmpty(
+          subRequirementDetailList.map((subr) => subr.studentDegree)
+        );
+        const minStudentDegree = validVals.length ? minNum(validVals) : null;
+        // 更新图表中个人达成情况
+        this.chartData[cindex].studentDegree = minStudentDegree || 0;
+
+        return {
+          name: column.name,
+          subRequirementDetailList,
+          studentDegree: minStudentDegree,
+        };
+      });
+      this.totalRowColMap = totalRowColMap;
+
+      const totalRequirementColumn = {
+        courseName: "毕业要求指标达成度",
+        semesterName: "",
+        credit: "",
+        requirementDetailList: totalObeRequirements,
+      };
+      this.dataList.push(totalRequirementColumn);
+
+      const totalColumn = {
+        courseName: "毕业要求达成度",
+        semesterName: "",
+        credit: "",
+        requirementDetailList: totalObeRequirements.map((item) => {
+          const nitem = { ...item };
+          nitem.subRequirementDetailList = item.subRequirementDetailList.map(
+            (elem, eindex) => {
+              const nelem = { ...elem };
+              nelem.studentDegree = item.studentDegree;
+              return nelem;
+            }
+          );
+          return nitem;
+        }),
+      };
+      this.dataList.push(totalColumn);
+    },
+    spanMethod({ rowIndex, columnIndex, column }) {
+      if (rowIndex === this.rowCount - 2) {
+        if (columnIndex === 0) {
+          return { rowspan: 1, colspan: 3 };
+        } else if (columnIndex <= 2) {
+          return [0, 0];
+        }
+      }
+
+      if (rowIndex === this.rowCount - 1) {
+        if (columnIndex === 0) {
+          return { rowspan: 1, colspan: 3 };
+        } else if (columnIndex <= 2) {
+          return [0, 0];
+        } else {
+          return this.totalRowColMap[columnIndex]
+            ? [1, this.totalRowColMap[columnIndex]]
+            : [0, 0];
+        }
+      }
+    },
+    updateChartOption() {
+      const option = {
+        color: ["#3a5ae5", "#fe5d4e"],
+        tooltip: { show: true },
+        grid: {
+          left: 30,
+          top: 60,
+          right: 20,
+          bottom: 20,
+          containLabel: true,
+        },
+        legend: {
+          top: 0,
+          data: ["个人达成情况", "专业达成情况"],
+          itemWidth: 16,
+          itemHeight: 4,
+          itemGap: 22,
+          left: 30,
+          textStyle: {
+            fontSize: 14,
+          },
+        },
+        xAxis: {
+          type: "category",
+          nameTextStyle: {
+            color: "#363D59",
+          },
+          data: this.chartData.map((item) => item.requirementName),
+          axisLabel: {
+            color: "#6F7482",
+            interval: 0,
+            fontSize: 14,
+            formatter: function (value, index) {
+              const vals = splitContent(value, 6);
+              vals[0] = `${index + 1}.${vals[0]} `;
+              return vals.join("\n");
+            },
+          },
+          axisLine: {
+            show: true,
+            lineStyle: {
+              color: "#EFF0F5",
+            },
+          },
+          splitLine: {
+            show: false,
+          },
+          axisTick: {
+            show: false,
+          },
+          axisPointer: {
+            type: "shadow",
+          },
+        },
+        yAxis: {
+          type: "value",
+          min: 0,
+          max: 1,
+          interval: 0.1,
+          nameTextStyle: {
+            color: "#363D59",
+          },
+          axisLabel: {
+            color: "#6F7482",
+          },
+          axisLine: {
+            lineStyle: {
+              color: "#EFF0F5",
+            },
+          },
+          splitLine: {
+            lineStyle: {
+              color: "#EFF0F5",
+            },
+          },
+        },
+        series: [
+          {
+            name: "个人达成情况",
+            type: "bar",
+            barWidth: 20,
+            data: this.chartData.map((item) => item.studentDegree),
+          },
+          {
+            name: "专业达成情况",
+            type: "bar",
+            barWidth: 20,
+            data: this.chartData.map((item) => item.professionalDegree),
+          },
+        ],
+      };
+
+      this.chartOption = option;
+    },
+  },
+};
+</script>

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

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

@@ -0,0 +1,140 @@
+<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="140px"
+      class="normal-score-form"
+    >
+      <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>

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

@@ -0,0 +1,138 @@
+<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="kid"
+      :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 [];
+      },
+    },
+    unuseTargets: {
+      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) => {
+        if (this.unuseTargets.includes(item.id)) {
+          item.disabled = true;
+        } else {
+          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>

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

@@ -0,0 +1,254 @@
+<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"></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 v-if="totalInfo" class="mt-2">{{ totalInfo }}</div>
+      <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"
+      :unuse-targets="unuseTargets"
+      @confirm="dimensionSelected"
+    ></select-blue-dimension-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  endScorePaperPositiveDetail,
+  endScorePaperPositiveSave,
+  endScorePaperPositiveSync,
+  courseOutlineTargetListPage,
+  courseExamineWeightDetail,
+} 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: [],
+      loading: false,
+      totalInfo: "",
+      unuseTargets: [],
+    };
+  },
+  watch: {
+    "course.obeCourseOutlineId": {
+      immediate: true,
+      handler(val, oldVal) {
+        if (!val) return;
+        if (val !== oldVal) this.getTree();
+      },
+    },
+  },
+  methods: {
+    async getTree() {
+      await this.getUnuseTarget();
+      const data = await courseOutlineTargetListPage({
+        obeCourseOutlineId: this.course.obeCourseOutlineId,
+      });
+      this.treeData = (data || []).map((item) => {
+        return {
+          id: item.id,
+          kid: item.id,
+          name: item.targetName,
+          totalWeight: item.totalWeight,
+          disabled: false,
+          children: item.dimensionList.map((elem) => {
+            return {
+              ...elem,
+              kid: `${item.id}_${elem.id}`,
+              disabled: false,
+            };
+          }),
+        };
+      });
+    },
+    async getUnuseTarget() {
+      const res = await courseExamineWeightDetail({
+        obeCourseOutlineId: this.course.obeCourseOutlineId,
+      });
+      const dataList = res.submitForm || [];
+
+      this.unuseTargets = dataList
+        .filter((item) => {
+          return !item.evaluationList[0].weight;
+        })
+        .map((item) => item.courseTargetId);
+    },
+    async getBlueDetail() {
+      const res = await endScorePaperPositiveDetail({
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
+      });
+      this.dataList = res || [];
+      this.updateTotalInfo();
+    },
+    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,
+        paperNumber: this.dataList[0]?.paperNumber,
+        examId: this.dataList[0]?.examId,
+      }).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;
+      }
+
+      return true;
+    },
+    toLink(row) {
+      this.curRow = row;
+      this.selectedData = [];
+      row.targetList.forEach((target) => {
+        target.dimensionList.forEach((dimension) => {
+          this.selectedData.push(`${target.targetId}_${dimension.dimensionId}`);
+        });
+      });
+      this.$refs.SelectBlueDimensionDialog.open();
+    },
+    dimensionSelected(targetList) {
+      this.curRow.targetList = targetList;
+      this.curRow.courseTargetName = targetList[0].targetName;
+      this.updateTotalInfo();
+    },
+    updateTotalInfo() {
+      const data = {};
+      this.dataList.forEach((item) => {
+        const target = item.targetList[0];
+        if (!target) return;
+
+        if (!data[target.targetName]) data[target.targetName] = 0;
+        data[target.targetName] += item.score;
+      });
+      this.totalInfo = Object.keys(data)
+        .map((key) => `${key}共${data[key]}分`)
+        .join(",");
+    },
+    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>

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

@@ -0,0 +1,105 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    title="同步成绩"
+    top="10vh"
+    width="600px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form ref="FilterForm" label-position="left" inline label-width="0px">
+      <el-form-item>
+        <exam-select
+          v-model="filter.examId"
+          :semester-id="filter.semesterId"
+          placeholder="考试"
+          :clearable="false"
+        ></exam-select>
+      </el-form-item>
+
+      <el-form-item>
+        <el-button type="primary" @click="getList">查询</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table ref="TableList" :data="dataList">
+      <el-table-column
+        type="index"
+        label="序号"
+        width="70"
+        :index="indexMethod"
+      ></el-table-column>
+      <el-table-column prop="paperNumber" label="试卷编号"> </el-table-column>
+      <el-table-column class-name="action-column" label="操作" width="80">
+        <template slot-scope="scope">
+          <el-button class="btn-primary" type="text" @click="toSync(scope.row)"
+            >同步</el-button
+          >
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import { endScoreSyncPaperList, endScoreSync } from "../../api";
+const initFilter = { semesterId: "", examId: "", courseId: "" };
+
+export default {
+  name: "SyncPaperDialog",
+  props: {
+    course: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      loading: false,
+      filter: { ...initFilter },
+      dataList: [],
+    };
+  },
+  methods: {
+    async getList() {
+      const res = await endScoreSyncPaperList(this.filter);
+      this.dataList = res || [];
+    },
+    visibleChange() {
+      this.filter = this.$objAssign(initFilter, this.course);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async toSync(row) {
+      if (this.loading) return;
+      this.loading = true;
+
+      const datas = {
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
+        examId: row.examId,
+        paperNumber: row.paperNumber,
+      };
+      const res = await endScoreSync(datas).catch(() => {});
+      this.loading = 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="course"
+      @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>

+ 936 - 0
src/modules/target/components/target-statistics/DetailTargetStatistics.vue

@@ -0,0 +1,936 @@
+<template>
+  <el-dialog
+    class="page-dialog"
+    :visible.sync="modalIsShow"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :show-close="false"
+    append-to-body
+    fullscreen
+    @opened="initData"
+  >
+    <div slot="title" class="box-justify">
+      <div>
+        <span>{{ course.courseName }}</span>
+      </div>
+      <div>
+        <el-button type="primary" :loading="downloading" @click="toSave"
+          >保存报告</el-button
+        >
+        <el-button type="primary" :loading="downloading" @click="toExport"
+          >导出报告</el-button
+        >
+        <el-button type="danger" @click="cancel">退出</el-button>
+      </div>
+    </div>
+    <div v-if="modalIsShow">
+      <!-- base info -->
+      <div class="page-head">
+        <h2>
+          <span>课程基本情况</span>
+        </h2>
+      </div>
+      <div class="part-box part-box-pad">
+        <table class="table table-tiny">
+          <colgroup>
+            <col width="100" />
+            <col />
+            <col width="100" />
+            <col />
+            <col width="110" />
+            <col />
+            <col width="100" />
+            <col />
+          </colgroup>
+          <tr>
+            <td class="td-bg">课程名称</td>
+            <td>{{ commonInfo.courseName }}</td>
+            <td class="td-bg">课程代码</td>
+            <td>{{ commonInfo.courseCode }}</td>
+            <td class="td-bg">课程英文名称</td>
+            <td>
+              <el-input
+                v-model.trim="courseBasicInfo.courseEnName"
+                placeholder="请填写"
+                size="small"
+                :maxlength="50"
+              ></el-input>
+            </td>
+            <td class="td-bg">学分</td>
+            <td>
+              <el-input-number
+                v-model="courseBasicInfo.credit"
+                placeholder="请录入学分"
+                size="small"
+                :min="0"
+                :max="999"
+                :controls="false"
+                style="width: 100px"
+              ></el-input-number>
+            </td>
+          </tr>
+          <tr>
+            <td class="td-bg">课程性质</td>
+            <td>
+              <el-input
+                v-model.trim="courseBasicInfo.courseType"
+                placeholder="请填写"
+                size="small"
+                :maxlength="50"
+              ></el-input>
+            </td>
+            <td class="td-bg">开课学院</td>
+            <td>{{ courseBasicInfo.college }}</td>
+            <td class="td-bg">开课专业</td>
+            <td>{{ courseBasicInfo.profession }}</td>
+            <td class="td-bg">学时</td>
+            <td>
+              <el-input-number
+                v-model="courseBasicInfo.period"
+                placeholder="请填写"
+                :min="0"
+                :max="999"
+                :controls="false"
+                size="small"
+                style="width: 100px"
+              ></el-input-number>
+            </td>
+          </tr>
+          <tr>
+            <td class="td-bg">开课班级</td>
+            <td>
+              <el-input
+                v-model.trim="courseBasicInfo.teachingObject"
+                placeholder="请填写"
+                size="small"
+                :maxlength="50"
+              ></el-input>
+            </td>
+            <td class="td-bg">开课学期</td>
+            <td>{{ courseBasicInfo.openTime }}</td>
+            <td class="td-bg">学生人数</td>
+            <td>{{ courseBasicInfo.participantCount }}</td>
+            <td class="td-bg">期望值</td>
+            <td>
+              {{ courseBasicInfo.courseDegree }}
+            </td>
+          </tr>
+          <tr>
+            <td class="td-bg">任课老师</td>
+            <td>
+              <el-input
+                v-model.trim="courseBasicInfo.teacher"
+                placeholder="请填写"
+                size="small"
+                :maxlength="50"
+              ></el-input>
+            </td>
+            <td class="td-bg">评价人</td>
+            <td colspan="5">
+              <el-input
+                v-model.trim="courseBasicInfo.director"
+                placeholder="请填写评价负责人姓名"
+                size="small"
+                :maxlength="50"
+              ></el-input>
+            </td>
+          </tr>
+        </table>
+      </div>
+
+      <div class="page-head">
+        <h2>
+          <span>课程目标考核分布</span>
+          <el-popover
+            popper-class="el-popper-dark"
+            placement="right-start"
+            width="500"
+            trigger="hover"
+          >
+            <i class="el-icon-question" slot="reference"></i>
+            <div>
+              <p>
+                课程目标分布会自动按题库系统里试卷下试题标记的属性内容进行显示
+              </p>
+            </div>
+          </el-popover>
+        </h2>
+      </div>
+      <div class="part-box part-box-pad">
+        <el-table :data="questionInfoTable">
+          <el-table-column
+            label="试题号"
+            prop="name"
+            width="200"
+            align="center"
+            fixed="left"
+          ></el-table-column>
+          <el-table-column
+            label="合计"
+            prop="totalScore"
+            width="60"
+            align="center"
+            fixed="left"
+          ></el-table-column>
+          <el-table-column
+            v-for="struct in paperStructs"
+            :key="struct.mainNumber"
+            :label="struct.mainNumber"
+            align="center"
+          >
+            <el-table-column
+              v-for="subNumber in struct.subNumbers"
+              :key="subNumber"
+              :label="subNumber"
+              :prop="`${struct.mainNumber}_${subNumber}`"
+              align="center"
+              min-width="50"
+            >
+            </el-table-column>
+          </el-table-column>
+        </el-table>
+        <div class="chart-box" style="height: 300px; margin-top: 20px">
+          <v-chart
+            v-if="questionInfoChartOption"
+            :option="questionInfoChartOption"
+          ></v-chart>
+        </div>
+      </div>
+
+      <div class="page-head">
+        <h2>
+          <span>课程目标达成评价结果</span>
+          <el-popover
+            popper-class="el-popper-dark"
+            placement="right-start"
+            width="500"
+            trigger="hover"
+          >
+            <i class="el-icon-question" slot="reference"></i>
+            <div>
+              <p>1.评价依据:填写通过考核什么内容实现课程目标的评价;</p>
+              <p>2.目标分值:对应考核环节的满分;</p>
+              <p>
+                3.权重:对应考核环节在对应的课程目标中的权重,目标下各项权重相加等于1;
+              </p>
+              <p>4.实际平均分:为参与评价的学生在该环节的平均分;</p>
+              <p>5.整体课程目标达成评价值:为课程分目标达成评价值的最小值。</p>
+            </div>
+          </el-popover>
+        </h2>
+      </div>
+      <div class="part-box part-box-pad">
+        <h4 class="part-title mb-2">课程考核成绩评价结果</h4>
+
+        <table class="table">
+          <tr class="td-bg">
+            <th>课程目标</th>
+            <th>评价依据</th>
+            <th>评价环节</th>
+            <th>权重</th>
+            <th>目标分值</th>
+            <th>实际平均分</th>
+            <th>目标达成评价值</th>
+          </tr>
+
+          <template v-for="item in courseTargetList">
+            <tr
+              v-for="(evaluation, eindex) in item.evaluationList"
+              :key="`${item.targetId}-${eindex}`"
+            >
+              <!-- 课程目标 -->
+              <td
+                v-if="!eindex"
+                :rowspan="item.evaluationList.length"
+                style="width: 240px"
+              >
+                {{ item.targetName }}
+              </td>
+              <!-- 评价依据 -->
+              <td v-if="!eindex" :rowspan="item.evaluationList.length">
+                <p
+                  v-for="edesc in item.graduationRequirementPoint.split(',')"
+                  :key="edesc"
+                >
+                  {{ edesc }}
+                </p>
+              </td>
+              <!-- 评价环节 -->
+              <td style="width: 140px">{{ evaluation.evaluation }}</td>
+              <!-- 权重 -->
+              <td style="width: 80px">
+                {{ evaluation.targetWeight }}
+              </td>
+              <!-- 目标分值 -->
+              <td style="width: 100px">{{ evaluation.targetScore }}</td>
+              <!-- 实际平均分 -->
+              <td style="width: 100px">{{ evaluation.targetAvgScore }}</td>
+              <!-- 目标达成评价值 -->
+              <td
+                v-if="!eindex"
+                :rowspan="item.evaluationList.length"
+                style="width: 140px"
+              >
+                {{ item.evaluationValue }}
+              </td>
+            </tr>
+          </template>
+          <tr>
+            <td colspan="6">课程总目标</td>
+            <td>{{ courseTargetValue }}</td>
+          </tr>
+        </table>
+
+        <div class="chart-box" style="height: 400px; margin-top: 20px">
+          <v-chart
+            v-if="courseTargetListChartOption"
+            :option="courseTargetListChartOption"
+          ></v-chart>
+        </div>
+
+        <h4 class="part-title mb-2">课程考核成绩评价明细结果</h4>
+
+        <el-table
+          :data="studentScoreTable"
+          :span-method="studentScoreSpanMethod"
+        >
+          <el-table-column
+            label="姓名"
+            prop="name"
+            min-width="120"
+            align="center"
+            fixed="left"
+          ></el-table-column>
+          <el-table-column
+            label="学号"
+            prop="studentCode"
+            width="160"
+            align="center"
+            fixed="left"
+          ></el-table-column>
+          <el-table-column
+            v-for="(target, tindex) in courseTargets"
+            :key="tindex"
+            :label="target.targetName"
+            align="center"
+          >
+            <el-table-column
+              v-if="target.finalDimensions.length"
+              :label="`期末成绩(${target.finalWeight}%)`"
+              :prop="`${target.targetId}-final`"
+              width="140"
+              align="center"
+            >
+            </el-table-column>
+            <el-table-column
+              v-for="work in target.usualWorks"
+              :key="`${target.targetId}${work.evaluation}`"
+              :label="`${work.evaluation}(${work.targetWeight}%)`"
+              :prop="`${target.targetId}-usual-${work.evaluation}`"
+              min-width="100"
+              align="center"
+            >
+            </el-table-column>
+          </el-table-column>
+          <el-table-column
+            label="综合成绩"
+            prop="score"
+            align="center"
+            width="100"
+          ></el-table-column>
+        </el-table>
+
+        <h4 class="part-title mb-2 mt-2">
+          课程目标达成度评价结果分析及改进措施
+        </h4>
+
+        <el-form label-position="top">
+          <el-form-item label="达成情况">
+            <el-input
+              v-model="courseBasicInfo.finishPoints"
+              type="textarea"
+              :autosize="{ minRows: 4, maxRows: 10 }"
+              resize="none"
+              placeholder="请输入"
+              clearable
+              maxlength="999"
+              show-word-limit
+            ></el-input>
+          </el-form-item>
+          <el-form-item label="课程支撑毕业要求达成情况评价">
+            <el-input
+              v-model="courseBasicInfo.requirementPoints"
+              type="textarea"
+              :autosize="{ minRows: 4, maxRows: 10 }"
+              resize="none"
+              placeholder="请输入"
+              clearable
+              maxlength="999"
+              show-word-limit
+            ></el-input>
+          </el-form-item>
+          <el-form-item label="课程持续改进">
+            <el-input
+              v-model="courseBasicInfo.courseSuggest"
+              type="textarea"
+              :autosize="{ minRows: 4, maxRows: 10 }"
+              resize="none"
+              placeholder="请输入"
+              clearable
+              maxlength="999"
+              show-word-limit
+            ></el-input>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+// import { reportData } from "./data";
+import {
+  targetStatisticsDetail,
+  targetStatisticsReport,
+  targetStatisticsSave,
+  targetStatisticsChangeCheck,
+} from "../../api";
+import { downloadByApi } from "@/plugins/download";
+import { calcSum } from "@/plugins/utils";
+
+export default {
+  name: "detail-target-statistics",
+  props: {
+    course: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  filters: {
+    percentFilter(val) {
+      const num = val || 0;
+      const perc = num % 10 ? 2 : 1;
+      return ((num || 0) / 100).toFixed(perc);
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      expectancyList: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
+      modalForm: {},
+      commonInfo: {},
+      courseBasicInfo: {},
+      paperStructs: [],
+      questionInfoTable: [],
+      courseTargetList: [],
+      courseTargetValue: 0.67,
+      questionInfoChartOption: null,
+      courseTargetListChartOption: null,
+      courseTargets: [],
+      targetColumnCounts: [],
+      studentScoreTable: [],
+      downloading: false,
+    };
+  },
+  methods: {
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    resetData() {
+      this.commonInfo = {};
+      this.courseBasicInfo = {};
+      this.paperStructs = [];
+      this.questionInfoTable = [];
+      this.courseTargetList = [];
+      this.courseTargetValue = 0.67;
+      this.questionInfoChartOption = null;
+      this.courseTargetListChartOption = null;
+      this.courseTargets = [];
+      this.targetColumnCounts = [];
+      this.studentScoreTable = [];
+      this.downloading = false;
+    },
+    buildData(data) {
+      this.commonInfo = data.commonInfo;
+      this.courseBasicInfo = {
+        ...data.courseBasicInfo,
+        finishPoints: data.finishPoints,
+        requirementPoints: data.requirementPoints,
+        courseSuggest: data.courseSuggest,
+      };
+
+      this.courseTargetValue =
+        data.courseEvaluationResultInfo.targetEvaluationSumValue;
+      this.courseTargetList = data.courseEvaluationResultInfo.targetList.map(
+        (target) => {
+          target.normalTargetWeight = calcSum(
+            target.evaluationList.slice(0, -1).map((elem) => elem.targetWeight)
+          );
+          return target;
+        }
+      );
+
+      const {
+        courseEvaluationSpreadInfo: { questionInfo, scoreList },
+      } = data;
+      this.parsePaperStructs(questionInfo);
+      this.parseQuestionInfoTable(questionInfo);
+      this.questionInfoChartOption = this.getQuestionInfoChartOption(scoreList);
+      this.courseTargetListChartOption = this.getCourseTargetListChartOption();
+
+      let examStudentList =
+        data.courseEvaluationResultDetailInfo.examStudentList;
+      examStudentList.pop();
+      examStudentList.splice(examStudentList.length - 2, 1);
+
+      this.parseCourseTargets(examStudentList);
+      this.parseStudentScoreTable(examStudentList);
+    },
+    async initData() {
+      this.resetData();
+
+      await this.checkChange();
+      const data = await targetStatisticsDetail({
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
+      });
+      this.buildData(data);
+    },
+    async checkChange() {
+      const res = await targetStatisticsChangeCheck({
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
+        report: true,
+      });
+
+      if (res.courseTargetChange) {
+        this.$notify.warning("课程目标与已保存不一致,请重新设置权重!");
+      }
+      if (res.evaluationChange) {
+        this.$notify.warning(
+          "评价方式与已保存不一致,请重新设置权重及导入新的平时成绩!"
+        );
+      }
+      if (res.targetScoreChange) {
+        this.$notify.warning(res.targetScoreChangeStr);
+      }
+    },
+    parsePaperStructs(questionInfo) {
+      if (!questionInfo || !questionInfo.length) return;
+
+      const structMap = {};
+      questionInfo.forEach((item) => {
+        if (!structMap[item.mainNumber]) {
+          structMap[item.mainNumber] = [];
+        }
+        structMap[item.mainNumber].push(item.subNumber + "");
+      });
+      this.paperStructs = Object.keys(structMap).map((mainNumber) => {
+        return {
+          mainNumber,
+          subNumbers: structMap[mainNumber],
+        };
+      });
+    },
+    parseQuestionInfoTable(questionInfo) {
+      const targetMap = {};
+      const tData = {
+        name: "目标分值",
+        totalScore: calcSum(questionInfo.map((item) => item.score || 0)),
+      };
+      questionInfo.forEach((question) => {
+        const qno = `${question.mainNumber}_${question.subNumber}`;
+        tData[qno] = question.score;
+
+        question.targetList.forEach((target) => {
+          if (!targetMap[target.targetId]) {
+            targetMap[target.targetId] = {
+              name: target.targetName,
+              totalScore: 0,
+            };
+          }
+
+          if (target.dimensionList.length) {
+            targetMap[target.targetId][qno] = question.score;
+            targetMap[target.targetId].totalScore += question.score;
+          } else {
+            targetMap[target.targetId][qno] = "";
+          }
+        });
+      });
+
+      this.questionInfoTable = [tData, ...Object.values(targetMap)];
+    },
+    getQuestionInfoChartOption(scoreList) {
+      const scoreRange = scoreList.scoreRange.map((item) => {
+        return {
+          name: `${item.minScore}~${item.maxScore}`,
+          value: item.rate,
+        };
+      });
+      scoreRange.unshift({
+        name: "不及格",
+        value: scoreList.failRate,
+      });
+
+      let options = {
+        color: ["#556dff", "#3A5AE5"],
+        title: {
+          text: "成绩分布图",
+          left: "center",
+        },
+        grid: {
+          left: 40,
+          top: 60,
+          right: 80,
+          bottom: 40,
+          containLabel: true,
+        },
+        xAxis: {
+          type: "category",
+          name: "成绩(分)",
+          nameTextStyle: {
+            color: "#363D59",
+          },
+          data: scoreRange.map((item) => item.name),
+          axisLabel: {
+            color: "#6F7482",
+            interval: 0,
+            fontSize: 12,
+            margin: 12,
+          },
+          axisLine: {
+            show: true,
+            lineStyle: {
+              color: "#EFF0F5",
+            },
+          },
+          splitLine: {
+            show: false,
+          },
+          axisTick: {
+            show: false,
+          },
+          axisPointer: {
+            type: "shadow",
+          },
+        },
+        yAxis: {
+          type: "value",
+          name: "百分比(%)",
+          nameTextStyle: {
+            color: "#363D59",
+          },
+          axisLabel: {
+            color: "#6F7482",
+          },
+          axisLine: {
+            lineStyle: {
+              color: "#EFF0F5",
+            },
+          },
+          splitLine: {
+            lineStyle: {
+              color: "#EFF0F5",
+            },
+          },
+        },
+        series: [
+          {
+            name: "占比",
+            type: "bar",
+            barWidth: 60,
+            data: scoreRange.map((item) => item.value),
+            label: {
+              show: true,
+              formatter: "{c}%",
+              position: "top",
+            },
+          },
+        ],
+      };
+      return options;
+    },
+    getCourseTargetListChartOption() {
+      let options = {
+        color: ["#556dff", "#f59a23"],
+        title: {
+          text: "课程目标达成评价值",
+          left: "center",
+        },
+        grid: {
+          left: 40,
+          top: 60,
+          right: 80,
+          bottom: 30,
+          containLabel: true,
+        },
+        legend: {
+          top: 0,
+          data: ["期望值", "达成值"],
+          itemWidth: 12,
+          itemHeight: 4,
+          itemGap: 22,
+          right: 40,
+        },
+        xAxis: {
+          type: "category",
+          name: "课程目标",
+          nameTextStyle: {
+            color: "#363D59",
+          },
+          data: this.courseTargetList.map((item) => item.targetName),
+          axisLabel: {
+            color: "#6F7482",
+            interval: 0,
+            fontSize: 12,
+            margin: 12,
+          },
+          axisLine: {
+            show: true,
+            lineStyle: {
+              color: "#EFF0F5",
+            },
+          },
+          splitLine: {
+            show: false,
+          },
+          axisTick: {
+            show: false,
+          },
+          axisPointer: {
+            type: "shadow",
+          },
+        },
+        yAxis: {
+          type: "value",
+          name: "期望值",
+          min: 0,
+          max: 1,
+          interval: 0.1,
+          nameTextStyle: {
+            color: "#363D59",
+          },
+          axisLabel: {
+            color: "#6F7482",
+          },
+          axisLine: {
+            lineStyle: {
+              color: "#EFF0F5",
+            },
+          },
+          splitLine: {
+            lineStyle: {
+              color: "#EFF0F5",
+            },
+          },
+        },
+        series: [
+          {
+            name: "达成值",
+            type: "bar",
+            barWidth: 60,
+            data: this.courseTargetList.map((item) => item.evaluationValue),
+            label: {
+              show: true,
+              position: "top",
+            },
+          },
+          {
+            name: "期望值",
+            type: "line",
+            symbol: "none",
+            data: this.courseTargetList.map(
+              (item) => this.courseBasicInfo.courseDegree || 0
+            ),
+          },
+        ],
+      };
+      return options;
+    },
+    parseCourseTargets(examStudentList) {
+      if (!examStudentList || !examStudentList.length) return;
+
+      const targetList = examStudentList[0].targetList;
+      const targetVals = {};
+      this.courseTargetList.forEach((target) => {
+        targetVals[target.targetId] = target.evaluationValue;
+      });
+
+      let tColumnCounts = [];
+      this.courseTargets = targetList.map((target) => {
+        const ntarget = {
+          targetId: target.targetId,
+          targetName: target.targetName,
+          finalWeight: target.finalScore?.targetWeight,
+          usualWeight: 0,
+          evaluationValue: targetVals[target.targetId],
+        };
+        ntarget.usualWeight = calcSum(
+          (target.usualScore?.scoreList || []).map((item) => item.targetWeight)
+        );
+        ntarget.finalDimensions = target.finalScore ? ["期末成绩"] : [];
+        ntarget.usualWorks = (target.usualScore?.scoreList || []).map(
+          (item) => {
+            return {
+              evaluation: item.evaluation,
+              targetWeight: item.targetWeight,
+            };
+          }
+        );
+        tColumnCounts.push(
+          ntarget.finalDimensions.length + ntarget.usualWorks.length
+        );
+        return ntarget;
+      });
+      // 只展示有期末成绩或平时成绩的目标
+      this.courseTargets = this.courseTargets.filter(
+        (item) => item.finalDimensions.length || item.usualWorks.length
+      );
+
+      let tCount = 0;
+      this.targetColumnCounts = tColumnCounts.map((item) => {
+        tCount += item;
+        return tCount;
+      });
+    },
+    getTargetFirseKey(target) {
+      if (target.finalDimensions.length) {
+        return `${target.targetId}-final`;
+      } else {
+        return `${target.targetId}-usual-${target.usualWorks[0].evaluation}`;
+      }
+    },
+    parseStudentScoreTable(examStudentList) {
+      if (!this.courseTargets.length) return;
+      const lastIndex = examStudentList.length - 1;
+      const studentScoreTable = examStudentList.map((student, sindex) => {
+        const nitem = {
+          name: student.name,
+          studentCode: student.studentCode,
+          score: student.score,
+        };
+
+        const finalScoreKey =
+          lastIndex === sindex ? "matrixAvgScore" : "targetScoreSum";
+        const workScoreKey = lastIndex === sindex ? "matrixAvgScore" : "score";
+
+        student.targetList.forEach((target, index) => {
+          nitem[`${target.targetId}-final`] =
+            target.finalScore && target.finalScore[finalScoreKey];
+
+          (target.usualScore?.scoreList || []).forEach((work) => {
+            nitem[`${target.targetId}-usual-${work.evaluation}`] =
+              work[workScoreKey];
+          });
+        });
+        return nitem;
+      });
+
+      const targetData = {
+        name: "课程目标达成度",
+        score: "",
+      };
+      this.courseTargets.forEach((target) => {
+        targetData[this.getTargetFirseKey(target)] = target.evaluationValue;
+      });
+      studentScoreTable.push(targetData);
+
+      const fTarget = this.courseTargets[0];
+      const fKey = this.getTargetFirseKey(fTarget);
+      studentScoreTable.push({
+        name: "课程达成度",
+        [fKey]: this.courseTargetValue,
+      });
+      this.studentScoreTable = studentScoreTable;
+    },
+    studentScoreSpanMethod({ row, column, rowIndex, columnIndex }) {
+      const lineCount = this.studentScoreTable.length - 1;
+      const maxTargetColumnCount = this.targetColumnCounts.slice(-1)[0];
+      if (rowIndex === lineCount) {
+        if (columnIndex === 0) {
+          return [1, 2];
+        } else if (columnIndex === 2) {
+          return [1, maxTargetColumnCount + 1];
+        } else {
+          return [0, 0];
+        }
+      }
+      if (rowIndex === lineCount - 1) {
+        const colsMap = {};
+        const targetColumns = [0, ...this.targetColumnCounts.slice(0, -1)];
+        targetColumns.map((item, index) => {
+          colsMap[item] = this.targetColumnCounts[index] - item;
+        });
+        if (columnIndex === 0) {
+          return [1, 2];
+        } else {
+          if (targetColumns.includes(columnIndex - 2)) {
+            return [1, colsMap[columnIndex - 2]];
+          } else if (columnIndex < maxTargetColumnCount + 2) {
+            return [0, 0];
+          } else {
+            return [1, 1];
+          }
+        }
+      }
+      if (rowIndex === lineCount - 2) {
+        if (columnIndex === 0) {
+          return [1, 2];
+        } else if (columnIndex === 1) {
+          return [0, 0];
+        } else {
+          return [1, 1];
+        }
+      }
+    },
+    async toSave() {
+      if (this.downloading) return;
+      this.downloading = true;
+
+      const res = await targetStatisticsSave({
+        cultureProgramId: this.course.cultureProgramId,
+        courseId: this.course.courseId,
+        ...this.courseBasicInfo,
+      }).catch(() => {});
+      this.downloading = false;
+
+      if (!res) return;
+      this.$message.success("保存成功!");
+      this.initData();
+    },
+    async toExport() {
+      if (this.downloading) return;
+      this.downloading = true;
+
+      const res = await downloadByApi(() => {
+        const datas = {
+          cultureProgramId: this.course.cultureProgramId,
+          courseId: this.course.courseId,
+        };
+        return targetStatisticsReport(datas);
+      }).catch((e) => {
+        this.$message.error(e || "下载失败,请重新尝试!");
+      });
+      this.downloading = false;
+
+      if (!res) return;
+      this.$message.success("下载成功!");
+    },
+  },
+};
+</script>
+
+<style scoped>
+.td-bg {
+  color: #8b8fa1;
+  font-weight: 500;
+}
+</style>

+ 187 - 0
src/modules/target/components/training-plan/AddTrainingPlanCourse.vue

@@ -0,0 +1,187 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    title="选择课程"
+    top="10vh"
+    width="800px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @opened="visibleChange"
+  >
+    <el-form ref="FilterForm" label-position="left" inline label-width="0px">
+      <el-form-item>
+        <org-select v-model="filter.orgId" placeholder="开课学院"></org-select>
+      </el-form-item>
+      <el-form-item>
+        <el-input
+          v-model.trim="filter.courseCodeOrName"
+          placeholder="课程名称或代码"
+          clearable
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item>
+        <el-button type="primary" :disabled="!canSearch" @click="toPage(1)"
+          >查询</el-button
+        >
+      </el-form-item>
+    </el-form>
+    <div class="box-justify mb-2">
+      <div>全部共{{ dataList.length }}门课</div>
+      <div>已选{{ multipleSelection.length }}门</div>
+    </div>
+    <el-table
+      ref="TableList"
+      :data="dataList"
+      border
+      max-height="500px"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column
+        type="selection"
+        fixed="left"
+        width="55"
+        align="center"
+        :selectable="(row) => row.canSelect"
+      ></el-table-column>
+      <el-table-column
+        type="index"
+        label="序号"
+        width="70"
+        :index="indexMethod"
+      ></el-table-column>
+      <el-table-column
+        prop="courseName"
+        label="课程名称"
+        min-width="120"
+      ></el-table-column>
+      <el-table-column
+        prop="courseCode"
+        label="课程编码"
+        min-width="120"
+      ></el-table-column>
+      <el-table-column
+        prop="orgName"
+        label="开课学院"
+        min-width="120"
+      ></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 slot="footer">
+      <el-button type="primary" :disabled="isSubmit" @click="submit"
+        >确认</el-button
+      >
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { trainingPlanCourseQueryList, trainingPlanCourseSave } from "../../api";
+
+export default {
+  name: "add-training-plan-course",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  computed: {
+    canSearch() {
+      return this.filter.orgId;
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      filter: {
+        cultureProgramId: "",
+        orgId: "",
+        courseCodeOrName: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      isSubmit: false,
+      dataList: [],
+      multipleSelection: [],
+    };
+  },
+  methods: {
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    visibleChange() {
+      this.filter.orgId = this.rowData.orgId;
+      this.filter.cultureProgramId = this.rowData.id;
+      this.toPage(1);
+      this.$nextTick(() => {
+        this.$refs.TableList?.clearSelection();
+      });
+    },
+    async getList() {
+      this.multipleSelection = [];
+      this.dataList = [];
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await trainingPlanCourseQueryList(datas);
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    handleSelectionChange(val) {
+      this.multipleSelection = val;
+    },
+    async submit() {
+      if (!this.multipleSelection.length) {
+        this.$message.error("请选择课程!");
+        return;
+      }
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const data = await trainingPlanCourseSave({
+        cultureProgramId: this.rowData.id,
+        courses: this.multipleSelection.map((item) => {
+          return {
+            courseId: item.courseId,
+          };
+        }),
+      }).catch(() => {});
+      this.isSubmit = false;
+
+      if (!data) return;
+      this.multipleSelection = [];
+      this.$message.success("选择成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 114 - 0
src/modules/target/components/training-plan/DetailTrainingPlan.vue

@@ -0,0 +1,114 @@
+<template>
+  <el-dialog
+    class="page-dialog"
+    :visible.sync="modalIsShow"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    fullscreen
+    @close="closeHandle"
+  >
+    <div slot="title">{{ rowData.name }}</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"
+        :row-data="dataInfo"
+        @update-detail="getDetail"
+      ></component>
+    </div>
+
+    <div slot="footer"></div>
+  </el-dialog>
+</template>
+
+<script>
+import { updateTrainingPlanDetail } from "../../api";
+import TrainingPlanBase from "./TrainingPlanBase.vue";
+import TrainingPlanTarget from "./TrainingPlanTarget.vue";
+import TrainingPlanRequirement from "./TrainingPlanRequirement.vue";
+import TrainingPlanMatrix from "./TrainingPlanMatrix.vue";
+import TrainingPlanCourse from "./TrainingPlanCourse.vue";
+import TrainingPlanCourseMatrix from "./TrainingPlanCourseMatrix.vue";
+
+export default {
+  name: "detail-training-plan",
+  components: {
+    TrainingPlanBase,
+    TrainingPlanTarget,
+    TrainingPlanRequirement,
+    TrainingPlanMatrix,
+    TrainingPlanCourse,
+    TrainingPlanCourseMatrix,
+  },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      curTab: "TrainingPlanBase",
+      tabs: [
+        {
+          name: "基本信息",
+          val: "TrainingPlanBase",
+        },
+        {
+          name: "培养目标",
+          val: "TrainingPlanTarget",
+        },
+        {
+          name: "毕业要求",
+          val: "TrainingPlanRequirement",
+        },
+        {
+          name: "培养目标与毕业要求关系矩阵",
+          val: "TrainingPlanMatrix",
+        },
+        {
+          name: "课程体系",
+          val: "TrainingPlanCourse",
+        },
+        {
+          name: "课程支撑毕业要求达成矩阵",
+          val: "TrainingPlanCourseMatrix",
+        },
+      ],
+      dataInfo: {},
+    };
+  },
+  methods: {
+    async getDetail() {
+      const res = await updateTrainingPlanDetail(this.rowData.id);
+      this.dataInfo = res || {};
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    selectMenu(tab) {
+      this.curTab = tab;
+    },
+    closeHandle() {
+      this.curTab = "TrainingPlanBase";
+    },
+  },
+};
+</script>

+ 116 - 0
src/modules/target/components/training-plan/ModifyTrainingPlan.vue

@@ -0,0 +1,116 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    :title="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"
+      :key="modalForm.id"
+      :rules="rules"
+      label-width="120px"
+    >
+      <el-form-item prop="name" label="培养方案名称:">
+        <el-input
+          v-model.trim="modalForm.name"
+          placeholder="培养方案名称"
+          clearable
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="professionalId" label="所属专业:">
+        <professional-select
+          v-model="modalForm.professionalId"
+          placeholder="所属专业"
+        ></professional-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 { updateTrainingPlan } from "../../api";
+
+const initModalForm = {
+  id: null,
+  professionalId: "",
+  name: "",
+};
+
+export default {
+  name: "modify-training-plan",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      rules: {
+        name: [
+          { required: true, message: "请输入培养方案名称", trigger: "change" },
+          {
+            message: "培养方案名称不能超过30个字",
+            max: 30,
+            trigger: "change",
+          },
+        ],
+        professionalId: [
+          { required: true, message: "请选择专业", trigger: "change" },
+        ],
+      },
+    };
+  },
+  computed: {
+    isEdit() {
+      return !!this.instance.id;
+    },
+    title() {
+      return (this.isEdit ? "编辑" : "新增") + "培养方案";
+    },
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = this.$objAssign(initModalForm, this.instance);
+    },
+    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 data = await updateTrainingPlan(this.modalForm).catch(() => {});
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 119 - 0
src/modules/target/components/training-plan/ModifyTrainingPlanRequirement.vue

@@ -0,0 +1,119 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    :title="title"
+    top="10vh"
+    width="600px"
+    :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"
+    >
+      <template v-if="isNode">
+        <el-form-item label="毕业要求:">
+          {{ instance.parentName }}
+        </el-form-item>
+        <el-form-item label="指标点:">
+          {{ instance.name }}
+        </el-form-item>
+      </template>
+      <template v-else>
+        <el-form-item label="毕业要求:">
+          {{ instance.name }}
+        </el-form-item>
+      </template>
+
+      <el-form-item label="内容:">
+        <el-input
+          v-model="modalForm.content"
+          type="textarea"
+          :autosize="{ minRows: 4, maxRows: 10 }"
+          resize="none"
+          placeholder="请输入内容"
+          clearable
+          maxlength="999"
+          show-word-limit
+        ></el-input>
+      </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 { updateTrainingPlanRequirement } from "../../api";
+
+const initModalForm = {
+  id: null,
+  parentId: "",
+  name: "",
+  content: "",
+};
+
+export default {
+  name: "modify-training-plan-requirement",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+    };
+  },
+  computed: {
+    isNode() {
+      return !!this.instance.parentId;
+    },
+    title() {
+      const typeName = this.isNode ? "指标点" : "毕业要求";
+      return `编辑${typeName}`;
+    },
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = this.$objAssign(initModalForm, this.instance);
+    },
+    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 data = await updateTrainingPlanRequirement(this.modalForm).catch(
+        () => {}
+      );
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 104 - 0
src/modules/target/components/training-plan/ModifyTrainingPlanRequirementPredict.vue

@@ -0,0 +1,104 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    title="毕业要求预期值"
+    top="10vh"
+    width="300px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :key="modalForm.id"
+      :rules="rules"
+    >
+      <el-form-item prop="expectValue" label="预期值:">
+        <el-input-number
+          v-model="modalForm.expectValue"
+          :min="0.01"
+          :max="1"
+          :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 { updateTrainingPlanRequirementPredict } from "../../api";
+
+const initModalForm = {
+  cultureProgramId: null,
+  expectValue: undefined,
+};
+
+export default {
+  name: "modify-training-plan-requirement-predict",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      rules: {
+        expectValue: [
+          {
+            required: true,
+            message: "请输入毕业要求预期值",
+            trigger: "change",
+          },
+        ],
+      },
+    };
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = {
+        cultureProgramId: this.rowData.id,
+        expectValue: this.rowData.expectValue || undefined,
+      };
+    },
+    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 data = await updateTrainingPlanRequirementPredict(
+        this.modalForm
+      ).catch(() => {});
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 125 - 0
src/modules/target/components/training-plan/ModifyTrainingPlanTarget.vue

@@ -0,0 +1,125 @@
+<template>
+  <el-dialog
+    :visible.sync="modalIsShow"
+    :title="title"
+    top="10vh"
+    width="800px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :key="modalForm.id"
+      :rules="rules"
+      label-width="120px"
+    >
+      <el-form-item prop="name" label="培养目标名称:">
+        <el-input
+          v-model.trim="modalForm.name"
+          placeholder="培养目标名称"
+          clearable
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="detail" label="目标分解详情:">
+        <el-input
+          v-model="modalForm.detail"
+          type="textarea"
+          :autosize="{ minRows: 4, maxRows: 10 }"
+          resize="none"
+          placeholder="请输入目标分解详情"
+          clearable
+          maxlength="999"
+          show-word-limit
+        ></el-input>
+      </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 { updateTrainingPlanTarget } from "../../api";
+
+const initModalForm = {
+  id: null,
+  cultureProgramId: "",
+  name: "",
+  detail: "",
+};
+
+export default {
+  name: "modify-training-plan-target",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      modalForm: { ...initModalForm },
+      rules: {
+        name: [
+          { required: true, message: "请输入培养目标名称", trigger: "change" },
+          {
+            message: "培养目标名称不能超过30个字",
+            max: 30,
+            trigger: "change",
+          },
+        ],
+        detail: [
+          { required: true, message: "请输入目标分解详情", trigger: "change" },
+        ],
+      },
+    };
+  },
+  computed: {
+    isEdit() {
+      return !!this.instance.id;
+    },
+    title() {
+      return (this.isEdit ? "编辑" : "新增") + "培养目标";
+    },
+  },
+  methods: {
+    visibleChange() {
+      this.modalForm = this.$objAssign(initModalForm, this.instance);
+    },
+    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 data = await updateTrainingPlanTarget(this.modalForm).catch(
+        () => {}
+      );
+      this.isSubmit = false;
+
+      if (!data) return;
+
+      this.$message.success("修改成功!");
+      this.$emit("modified");
+      this.cancel();
+    },
+  },
+};
+</script>

+ 45 - 0
src/modules/target/components/training-plan/TrainingPlanBase.vue

@@ -0,0 +1,45 @@
+<template>
+  <div class="training-plan-base part-box part-box-pad">
+    <el-descriptions title="培养方案" :column="1" size="medium">
+      <el-descriptions-item label="培养方案名称">{{
+        rowData.name
+      }}</el-descriptions-item>
+      <el-descriptions-item label="专业">{{
+        rowData.professionalName
+      }}</el-descriptions-item>
+      <el-descriptions-item label="培养目标">
+        {{ rowData.targetCount }}个
+      </el-descriptions-item>
+      <el-descriptions-item label="毕业要求">
+        {{ rowData.requirementCount }}项
+      </el-descriptions-item>
+      <el-descriptions-item label="课程体系">
+        {{ rowData.courseCount }}门
+      </el-descriptions-item>
+      <el-descriptions-item label="创建人">
+        {{ rowData.createRealName }}({{ rowData.createLoginName }})
+      </el-descriptions-item>
+    </el-descriptions>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "training-plan-base",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {};
+  },
+  mounted() {
+    this.$emit("update-detail");
+  },
+  methods: {},
+};
+</script>

+ 146 - 0
src/modules/target/components/training-plan/TrainingPlanCourse.vue

@@ -0,0 +1,146 @@
+<template>
+  <div class="training-plan-matrix">
+    <div class="part-box part-box-pad">
+      <div class="box-justify mb-2">
+        <div></div>
+        <el-button type="primary" icon="el-icon-add" @click="toAdd"
+          >选择课程</el-button
+        >
+      </div>
+      <el-table ref="TableList" :data="dataList">
+        <el-table-column type="index" label="序号" width="70"></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
+          class-name="action-column"
+          label="操作"
+          width="140"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              class="btn-danger"
+              type="text"
+              @click="toDelete(scope.row)"
+              >删除</el-button
+            >
+            <el-button
+              v-if="scope.$index !== 0"
+              class="btn-primary"
+              type="text"
+              @click="toMove(scope.$index, 'up')"
+              >上移</el-button
+            >
+            <el-button
+              v-if="scope.$index !== total - 1"
+              class="btn-primary"
+              type="text"
+              @click="toMove(scope.$index, 'down')"
+              >下移</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- AddTrainingPlanCourse -->
+    <add-training-plan-course
+      ref="AddTrainingPlanCourse"
+      :row-data="rowData"
+      @modified="getList"
+    ></add-training-plan-course>
+  </div>
+</template>
+
+<script>
+import {
+  trainingPlanCourseListPage,
+  deleteRrainingPlanCourse,
+  sortRrainingPlanCourse,
+} from "../../api";
+
+import AddTrainingPlanCourse from "./AddTrainingPlanCourse.vue";
+
+export default {
+  name: "training-plan-course",
+  components: {
+    AddTrainingPlanCourse,
+  },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      dataList: [],
+      total: 0,
+    };
+  },
+  mounted() {
+    this.getList();
+  },
+  methods: {
+    async getList() {
+      const data = await trainingPlanCourseListPage({
+        cultureProgramId: this.rowData.id,
+      });
+      this.dataList = data || [];
+      this.total = this.dataList.length;
+    },
+    toAdd() {
+      this.$refs.AddTrainingPlanCourse.open();
+    },
+    exchangeSortNumber(row1, row2) {
+      const sortNum = row1.sortNum;
+      row1.sortNum = row2.sortNum;
+      row2.sortNum = sortNum;
+    },
+    async toMove(rowIndex, type) {
+      const row1 = this.dataList[rowIndex];
+      const row2 =
+        type === "up"
+          ? this.dataList[rowIndex - 1]
+          : this.dataList[rowIndex + 1];
+
+      this.exchangeSortNumber(row1, row2);
+
+      const datas = this.dataList.map((item, index) => {
+        return {
+          id: item.id,
+          sortNum: item.sortNum,
+        };
+      });
+
+      const res = await sortRrainingPlanCourse(datas).catch(() => {
+        this.exchangeSortNumber(row1, row2);
+      });
+      if (!res) return;
+
+      this.dataList.sort((a, b) => a.sortNum - b.sortNum);
+    },
+    async toDelete(row) {
+      const confirm = await this.$confirm(
+        `确定要删除课程【${row.courseName}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await deleteRrainingPlanCourse(row.id);
+      this.$message.success("删除成功!");
+      this.getList();
+    },
+  },
+};
+</script>

+ 339 - 0
src/modules/target/components/training-plan/TrainingPlanCourseMatrix.vue

@@ -0,0 +1,339 @@
+<template>
+  <div class="part-box part-box-pad course-matrix">
+    <el-table
+      :data="dataList"
+      border
+      :cell-style="cellStyleHandle"
+      :summary-method="getSummaries"
+      show-summary
+    >
+      <el-table-column label="课程信息" align="center" fixed="left" width="300">
+        <el-table-column
+          label="课程名称(代码)"
+          width="240"
+          show-overflow-tooltip
+        >
+          <template slot-scope="scope">
+            {{ scope.row.courseName }}({{ scope.row.courseCode }})
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="学分"
+          prop="credit"
+          width="60"
+          align="center"
+        ></el-table-column>
+      </el-table-column>
+      <el-table-column label="毕业要求" align="center">
+        <template v-if="hasSubRequirements">
+          <el-table-column
+            v-for="(column, cindex) in columns"
+            :key="cindex"
+            :label="column.name"
+            align="center"
+          >
+            <el-table-column
+              v-for="subr in column.subRequirements"
+              :key="subr.name"
+              align="center"
+              :prop="`${column.name}_${subr.name}`"
+            >
+              <template slot="header">
+                <span v-if="subr.name === 'null'"></span>
+                <template v-else>
+                  <span>{{ subr.name }}</span>
+                  <el-tooltip v-if="subr.content" effect="dark" placement="top">
+                    <div slot="content" class="tooltip-area">
+                      {{ subr.content }}
+                    </div>
+                    <i class="el-icon-info ml-1 tooltip-info-icon"></i>
+                  </el-tooltip>
+                </template>
+              </template>
+              <template slot-scope="scope">
+                <el-input-number
+                  v-if="scope.row.canEdit"
+                  v-model="scope.row[`${column.name}_${subr.name}`].value"
+                  class="width-50"
+                  :min="0"
+                  :max="1"
+                  :step="0.01"
+                  step-strictly
+                  :controls="false"
+                  size="mini"
+                  @change="
+                    () =>
+                      unitChange(
+                        scope.row,
+                        `${column.name}_${subr.name}`,
+                        subr.columnIndex
+                      )
+                  "
+                ></el-input-number>
+                <span v-else>{{
+                  scope.row[`${column.name}_${subr.name}`].value
+                }}</span>
+              </template>
+            </el-table-column>
+          </el-table-column>
+        </template>
+
+        <template v-else>
+          <el-table-column
+            v-for="(column, cindex) in columns"
+            :key="cindex"
+            :label="column.name"
+            align="center"
+            :prop="`${column.name}_null`"
+          >
+            <template slot-scope="scope">
+              <el-input-number
+                v-if="scope.row.canEdit"
+                v-model="scope.row[`${column.name}_null`].value"
+                class="width-50"
+                :min="0"
+                :max="1"
+                :step="0.01"
+                step-strictly
+                :controls="false"
+                size="mini"
+                @change="
+                  () =>
+                    unitChange(
+                      scope.row,
+                      `${column.name}_null`,
+                      column.columnIndex
+                    )
+                "
+              ></el-input-number>
+              <span v-else>{{ scope.row[`${column.name}_null`].value }}</span>
+            </template>
+          </el-table-column>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import {
+  trainingPlanCourseMatrixDetail,
+  trainingPlanCourseMatrixSave,
+} from "../../api";
+import { calcSum } from "@/plugins/utils";
+
+export default {
+  name: "training-plan-course-matrix",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      dataList: [],
+      columns: [],
+      hasSubRequirements: false,
+      loading: false,
+      errorIndexs: [],
+      warningColumnIndexs: [],
+    };
+  },
+  mounted() {
+    this.getList();
+  },
+  methods: {
+    async getList() {
+      const res = await trainingPlanCourseMatrixDetail({
+        cultureProgramId: this.rowData.id,
+      });
+      const tableData = res || [];
+      this.dataList = tableData.map((item, index) => {
+        if (!index) this.parseColumns(item.requirements);
+
+        const nitem = {
+          courseId: item.courseId,
+          courseCode: item.courseCode,
+          courseName: item.courseName,
+          canEdit: item.canEdit,
+        };
+        item.requirements.forEach((requirement) => {
+          requirement.subRequirements.forEach((subr) => {
+            nitem[`${requirement.name}_${subr.name + ""}`] = {
+              id: subr.id,
+              value: subr.scale || undefined,
+            };
+          });
+        });
+
+        return nitem;
+      });
+      this.initWarningColumnIndexs();
+    },
+    parseColumns(requirements) {
+      this.hasSubRequirements = requirements.some(
+        (item) => item.subRequirements[0].name !== null
+      );
+      if (!this.hasSubRequirements) {
+        this.columns = requirements.map((item, index) => {
+          return { name: item.name, columnIndex: index + 1 };
+        });
+        return;
+      }
+
+      let cindex = 1;
+      this.columns = requirements.map((item) => {
+        return {
+          name: item.name,
+          subRequirements: item.subRequirements.map((subr) => {
+            return {
+              name: subr.name + "",
+              columnIndex: ++cindex,
+              content: subr.content,
+            };
+          }),
+        };
+      });
+    },
+    updateErrorIndexs([rowIndex, columnIndex]) {
+      const pos = this.errorIndexs.findIndex((item) => item[1] === columnIndex);
+      if (pos !== -1) {
+        this.errorIndexs.splice(pos, 1);
+      }
+
+      this.errorIndexs.push([rowIndex, columnIndex]);
+    },
+    clearErrorIndexs(columnIndex) {
+      this.errorIndexs = this.errorIndexs.filter(
+        (item) => item[1] !== columnIndex
+      );
+    },
+    initWarningColumnIndexs() {
+      const warningColumnIndexs = [];
+      this.columns.forEach((column) => {
+        column.subRequirements.forEach((subr) => {
+          const key = `${column.name}_${subr.name}`;
+          const totalVal = calcSum(
+            this.dataList.map((item) => item[key].value || 0)
+          );
+          if (totalVal < 1) warningColumnIndexs.push(subr.columnIndex);
+        });
+      });
+      this.warningColumnIndexs = warningColumnIndexs;
+    },
+    updateWarningColumnIndexs(columnIndex, type) {
+      const pos = this.warningColumnIndexs.findIndex(
+        (item) => item === columnIndex
+      );
+      if (pos !== -1) {
+        if (type === "remove") {
+          this.warningColumnIndexs.splice(pos, 1);
+        }
+      } else {
+        if (type === "add") {
+          this.warningColumnIndexs.push(columnIndex);
+        }
+      }
+    },
+    async unitChange(row, key, columnIndex) {
+      const [fieldName, nodeName] = key.split("_");
+      const totalVal = calcSum(
+        this.dataList.map((item) => item[key].value || 0)
+      );
+      if (totalVal < 1) {
+        this.updateWarningColumnIndexs(columnIndex, "add");
+      } else {
+        this.updateWarningColumnIndexs(columnIndex, "remove");
+      }
+
+      if (totalVal > 1) {
+        const columnName =
+          nodeName === "null" ? fieldName : `${fieldName}:${nodeName}`;
+        this.$message.error(`${columnName}列总和大于1,当前修改值将不会保存!`);
+
+        const rowIndex = this.dataList.findIndex(
+          (item) => item.courseCode === row.courseCode
+        );
+
+        this.updateErrorIndexs([rowIndex, columnIndex]);
+        return;
+      }
+
+      let saveData = [];
+      const curColumnHasError = this.errorIndexs.some(
+        (item) => item[1] === columnIndex
+      );
+      if (curColumnHasError) {
+        saveData = this.dataList.map((item) => {
+          return {
+            id: item[key].id,
+            scale: item[key].value,
+          };
+        });
+      } else {
+        saveData = [
+          {
+            id: row[key].id,
+            scale: row[key].value,
+          },
+        ];
+      }
+
+      this.clearErrorIndexs(columnIndex);
+      await trainingPlanCourseMatrixSave(saveData);
+    },
+    cellStyleHandle({ rowIndex, columnIndex }) {
+      let style = undefined;
+      if (this.warningColumnIndexs.includes(columnIndex)) {
+        style = {
+          backgroundColor: "#fdecdc",
+        };
+      }
+      if (!this.errorIndexs.length) return style;
+
+      if (
+        this.errorIndexs.some(
+          (item) => rowIndex === item[0] && columnIndex === item[1]
+        )
+      ) {
+        style = {
+          backgroundColor: "#fde2e2",
+        };
+      }
+
+      return style;
+    },
+
+    // spanMethod({ rowIndex, columnIndex }) {
+    //   if (rowIndex === 0 && columnIndex === 0) {
+    //     return { rowspan: 2, colspan: this.hasSubRequirements ? 2 : 1 };
+    //   }
+    // },
+    getSummaries(param) {
+      const { columns } = param;
+      const sums = [];
+      columns.forEach((column, index) => {
+        if (index === 0) {
+          sums[index] = "合计";
+          return;
+        }
+        if (index === 1) {
+          sums[index] = "";
+          return;
+        }
+
+        const total = calcSum(
+          this.dataList.map((item) =>
+            item[column.property].value ? item[column.property].value * 100 : 0
+          )
+        );
+        sums[index] = (total / 100).toFixed(2);
+      });
+      return sums;
+    },
+  },
+};
+</script>

+ 84 - 0
src/modules/target/components/training-plan/TrainingPlanMatrix.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="training-plan-matrix part-box part-box-pad">
+    <div class="part-box part-box-pad">
+      <el-table v-if="columns.length" :data="dataList" border>
+        <el-table-column
+          label="毕业要求"
+          prop="requirementName"
+          min-width="200"
+          fixed="left"
+        >
+        </el-table-column>
+        <el-table-column label="培养目标" align="center">
+          <el-table-column
+            v-for="(column, cindex) in columns"
+            :key="cindex"
+            :label="column"
+            align="center"
+          >
+            <template slot-scope="scope">
+              <el-checkbox
+                v-model="scope.row.targetList[cindex].enable"
+                @change="matrixChange"
+              ></el-checkbox>
+            </template>
+          </el-table-column>
+        </el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  trainingPlanMatrixListPage,
+  updateRrainingPlanMatrix,
+} from "../../api";
+
+export default {
+  name: "training-plan-matrix",
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      dataList: [],
+      columns: [],
+    };
+  },
+  mounted() {
+    this.getList();
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        cultureProgramId: this.rowData.id,
+      };
+      const res = await trainingPlanMatrixListPage(datas);
+      if (!res || !res.length) return;
+
+      this.columns = res[0].targetList.map((item) => item.name);
+      this.dataList = res;
+    },
+    async matrixChange() {
+      const list = this.dataList
+        .map((item) => {
+          return item.targetList
+            .filter((elem) => elem.enable)
+            .map((elem) => elem.id);
+        })
+        .flat();
+
+      await updateRrainingPlanMatrix({
+        cultureProgramId: this.rowData.id,
+        list,
+      });
+    },
+  },
+};
+</script>

+ 160 - 0
src/modules/target/components/training-plan/TrainingPlanRequirement.vue

@@ -0,0 +1,160 @@
+<template>
+  <div class="training-plan-requirement">
+    <div class="part-box part-box-pad">
+      <div class="box-justify mb-2">
+        <div></div>
+        <el-button type="primary" icon="el-icon-add" @click="toEditPredict"
+          >预期值</el-button
+        >
+      </div>
+      <el-table
+        ref="TableList"
+        :data="dataList"
+        row-key="id"
+        default-expand-all
+      >
+        <el-table-column prop="label" label="毕业要求及指标点" width="200">
+        </el-table-column>
+        <el-table-column prop="content" label="内容" min-width="300">
+        </el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          width="180"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              v-if="scope.row.children"
+              class="btn-primary"
+              type="text"
+              @click="toAddNode(scope.row)"
+              >新增指标点</el-button
+            >
+            <el-button
+              v-else
+              class="btn-danger"
+              type="text"
+              @click="toDeleteNode(scope.row)"
+              >删除</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- ModifyTrainingPlanRequirement -->
+    <modify-training-plan-requirement
+      ref="ModifyTrainingPlanRequirement"
+      :instance="curRow"
+      @modified="getList"
+    ></modify-training-plan-requirement>
+    <!-- ModifyTrainingPlanRequirementPredict -->
+    <modify-training-plan-requirement-predict
+      ref="ModifyTrainingPlanRequirementPredict"
+      :row-data="rowData"
+      @modified="predictChange"
+    ></modify-training-plan-requirement-predict>
+  </div>
+</template>
+
+<script>
+import {
+  trainingPlanRequirementListPage,
+  deleteTrainingPlanRequirementNode,
+  addTrainingPlanRequirementNode,
+} from "../../api";
+
+import ModifyTrainingPlanRequirement from "./ModifyTrainingPlanRequirement.vue";
+import ModifyTrainingPlanRequirementPredict from "./ModifyTrainingPlanRequirementPredict.vue";
+
+export default {
+  name: "training-plan-requirement",
+  components: {
+    ModifyTrainingPlanRequirement,
+    ModifyTrainingPlanRequirementPredict,
+  },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      dataList: [],
+      curRow: {},
+    };
+  },
+  mounted() {
+    this.getList();
+  },
+  methods: {
+    async getList() {
+      const data = await trainingPlanRequirementListPage({
+        cultureProgramId: this.rowData.id,
+      });
+      this.dataList = (data || []).map((item) => {
+        item.label = `${item.sortNum}.${item.name}`;
+        item.children = item.subRequirementList.map((elem) => {
+          const nelem = { ...elem };
+          nelem.label = `指标点${item.sortNum}-${elem.sortNum}`;
+          nelem.parentName = item.name;
+          delete nelem.subRequirementList;
+          return nelem;
+        });
+        return item;
+      });
+    },
+    async toAddNode(row) {
+      await addTrainingPlanRequirementNode({
+        cultureProgramId: this.rowData.id,
+        id: row.id,
+      });
+      await this.getList();
+    },
+    toEdit(row) {
+      this.curRow = row;
+      this.$refs.ModifyTrainingPlanRequirement.open();
+    },
+    toEditPredict() {
+      this.$refs.ModifyTrainingPlanRequirementPredict.open();
+    },
+    predictChange() {
+      this.$emit("update-detail");
+    },
+    async toDeleteNode(row) {
+      if (row.parentId) {
+        const parentRow = this.dataList.find(
+          (item) => item.id === row.parentId
+        );
+        if (parentRow.children.length <= 1) {
+          this.$message.error("当前毕业要求必须有一个指标点!");
+          return;
+        }
+      }
+
+      const confirm = await this.$confirm(
+        `确定要删除 ${row.parentName} 【${row.label}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await deleteTrainingPlanRequirementNode(row.id);
+      this.$message.success("删除成功!");
+      this.getList();
+    },
+  },
+};
+</script>

+ 143 - 0
src/modules/target/components/training-plan/TrainingPlanTarget.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="training-plan-target">
+    <div class="part-box part-box-pad">
+      <el-form>
+        <el-form-item label="总体描述:">
+          <el-input
+            v-model="modalForm.description"
+            type="textarea"
+            :autosize="{ minRows: 4, maxRows: 10 }"
+            resize="none"
+            placeholder="请输入总体描述"
+            clearable
+            maxlength="999"
+            show-word-limit
+            @blur="descriptionChange"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="part-box part-box-pad">
+      <div class="box-justify mb-2">
+        <div></div>
+        <el-button type="primary" icon="el-icon-add" @click="toAdd"
+          >新增</el-button
+        >
+      </div>
+      <el-table ref="TableList" :data="dataList">
+        <el-table-column
+          type="index"
+          label="序号"
+          width="70"
+          :index="indexMethod"
+        ></el-table-column>
+        <el-table-column prop="name" label="培养目标名称" min-width="160">
+        </el-table-column>
+        <el-table-column prop="detail" label="目标分解详情" min-width="300">
+        </el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          width="120"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              class="btn-danger"
+              type="text"
+              @click="toDelete(scope.row)"
+              >删除</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- ModifyTrainingPlanTarget -->
+    <modify-training-plan-target
+      ref="ModifyTrainingPlanTarget"
+      :instance="curRow"
+      @modified="getList"
+    ></modify-training-plan-target>
+  </div>
+</template>
+
+<script>
+import {
+  trainingPlanTargetListPage,
+  deleteTrainingPlanTarget,
+  updateTrainingPlanDescription,
+} from "../../api";
+import ModifyTrainingPlanTarget from "./ModifyTrainingPlanTarget.vue";
+
+export default {
+  name: "training-plan-target",
+  components: { ModifyTrainingPlanTarget },
+  props: {
+    rowData: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      modalForm: {
+        description: "",
+      },
+      dataList: [],
+      curRow: {},
+    };
+  },
+  mounted() {
+    this.modalForm.description = this.rowData.description;
+    this.getList();
+  },
+  methods: {
+    async getList() {
+      const datas = {
+        cultureProgramId: this.rowData.id,
+      };
+      const data = await trainingPlanTargetListPage(datas);
+      this.dataList = data || [];
+    },
+    toAdd() {
+      this.curRow = { cultureProgramId: this.rowData.id };
+      this.$refs.ModifyTrainingPlanTarget.open();
+    },
+    toEdit(row) {
+      this.curRow = row;
+      this.$refs.ModifyTrainingPlanTarget.open();
+    },
+    async toDelete(row) {
+      const confirm = await this.$confirm(
+        `确定要删除培养目标【${row.name}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await deleteTrainingPlanTarget(row.id);
+      this.$message.success("删除成功!");
+      this.deletePageLastItem();
+    },
+    async descriptionChange() {
+      await updateTrainingPlanDescription({
+        id: this.rowData.id,
+        description: this.modalForm.description,
+      });
+      this.$emit("update-detail");
+    },
+  },
+};
+</script>

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

@@ -0,0 +1,45 @@
+import TrainingPlanManage from "./views/TrainingPlanManage.vue";
+import CourseOutlineManage from "./views/CourseOutlineManage.vue";
+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 [
+  {
+    path: "/target/training-plan-manage",
+    name: "CultureProgram",
+    component: TrainingPlanManage,
+  },
+  {
+    path: "/target/course-outline-manage",
+    name: "CourseOutlineManager",
+    component: CourseOutlineManage,
+  },
+  {
+    path: "/target/course-examine",
+    name: "CourseAssessmentSetting",
+    component: CourseExamine,
+  },
+  {
+    path: "/target/requirement-statistics",
+    name: "GraduationRequirement",
+    component: RequirementStatistics,
+  },
+  {
+    path: "/target/target-statistics",
+    name: "TargetReportManage",
+    component: TargetStatistics,
+  },
+  {
+    path: "/target/student-target",
+    name: "StudentRequirement",
+    component: StudentTarget,
+  },
+  {
+    path: "/target/target-score-manage",
+    name: "TargetScoreManage",
+    component: TargetScoreManage,
+  },
+];

+ 35 - 0
src/modules/target/store.js

@@ -0,0 +1,35 @@
+import { courseExamineWeightSettingStatus } from "./api";
+
+const state = {
+  cwStatus: {
+    // 曾经设置过情况,true:曾经设置过(编辑课程设置时警告提示),false:从未设置过(不需要提示)
+    everSettingStatus: false,
+    // 当前设置情况,true:当前课程设置过权重,权重设置界面需要展示两个表单,false:当前未设置权重,权重设置界面只展示1个表单
+    currentSettingStatus: false,
+    // 教学课程管理者(创建人)id 当前登录人和课程创建人一致时“教学班”“教师团队”两个页签才能编辑,否则只能查看
+    courseCreateId: "",
+  },
+};
+
+const mutations = {
+  setCwStatus(state, data) {
+    state.cwStatus = Object.assign({}, state.cwStatus, data);
+  },
+  resetState(state) {
+    state.cwStatus = {};
+  },
+};
+
+const actions = {
+  async updateCwStatus({ commit }, data) {
+    const res = await courseExamineWeightSettingStatus(data);
+    commit("setCwStatus", res || {});
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 185 - 0
src/modules/target/views/CourseExamine.vue

@@ -0,0 +1,185 @@
+<template>
+  <div class="training-plan-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="专业:">
+            <professional-select
+              v-model="filter.professionalId"
+              placeholder="专业"
+            ></professional-select>
+          </el-form-item>
+          <el-form-item label="培养方案:">
+            <training-plan-select
+              v-model="filter.cultureProgramId"
+              placeholder="培养方案"
+              :professional-id="filter.professionalId"
+              professional-required
+            ></training-plan-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="55"
+          :index="indexMethod"
+        ></el-table-column>
+        <el-table-column prop="courseName" label="课程名称" min-width="200">
+        </el-table-column>
+        <el-table-column prop="courseCode" label="课程代码" width="120">
+        </el-table-column>
+        <el-table-column
+          prop="outlineName"
+          label="课程大纲名称"
+          min-width="300"
+        >
+        </el-table-column>
+        <el-table-column prop="evaluationMode" label="考核方式" width="100">
+          <span slot-scope="scope">
+            {{ scope.row.evaluationMode | evaluationModeFilter }}
+          </span>
+        </el-table-column>
+        <el-table-column prop="userName" label="负责教师" min-width="120">
+        </el-table-column>
+        <el-table-column
+          prop="targetCount"
+          label="课程目标数量"
+          width="110"
+        ></el-table-column>
+        <el-table-column prop="weightSetting" label="权重设置状态" width="110">
+          <template slot-scope="scope">
+            <span :class="scope.row.weightSetting ? '' : 'color-gray-2'">
+              {{ scope.row.weightSetting ? "已完成" : "未完成" }}
+            </span>
+          </template>
+        </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', 'edit')"
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              v-if="checkPrivilege('link', 'delete')"
+              class="btn-danger"
+              type="text"
+              @click="toDelete(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>
+    <!-- DetailCourseExamine -->
+    <detail-course-examine
+      v-if="checkPrivilege('link', 'edit')"
+      ref="DetailCourseExamine"
+      :row-data="curRow"
+      @modified="getList"
+    ></detail-course-examine>
+  </div>
+</template>
+
+<script>
+import { courseExamineListPage, deleteCourseExamine } from "../api";
+import DetailCourseExamine from "../components/course-examine/DetailCourseExamine.vue";
+
+export default {
+  name: "course-examine-manage",
+  components: { DetailCourseExamine },
+  data() {
+    return {
+      filter: {
+        professionalId: "",
+        cultureProgramId: "",
+        courseId: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+    };
+  },
+  mounted() {
+    this.toPage(1);
+  },
+  methods: {
+    async getList() {
+      if (!this.checkPrivilege("list", "list")) return;
+
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await courseExamineListPage(datas);
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    search() {
+      this.toPage(1);
+    },
+    toEdit(row) {
+      this.curRow = row;
+      this.$refs.DetailCourseExamine.open();
+    },
+    async toDelete(row) {
+      const confirm = await this.$confirm(`确定要删除当前记录吗?`, "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await deleteCourseExamine(row.id);
+      this.$message.success("删除成功!");
+      this.deletePageLastItem();
+    },
+  },
+};
+</script>

+ 239 - 0
src/modules/target/views/CourseOutlineManage.vue

@@ -0,0 +1,239 @@
+<template>
+  <div class="course-outline-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="专业:">
+            <professional-select
+              v-model="filter.professionalId"
+              placeholder="专业"
+            ></professional-select>
+          </el-form-item>
+          <el-form-item label="培养方案:">
+            <training-plan-select
+              v-model="filter.cultureProgramId"
+              placeholder="培养方案"
+              :professional-id="filter.professionalId"
+              professional-required
+            ></training-plan-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>
+          <el-form-item label="课程大纲名称:">
+            <el-input
+              v-model.trim="filter.name"
+              placeholder="课程大纲名称"
+              clearable
+            ></el-input>
+          </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 class="part-box-action">
+        <el-button
+          v-if="checkPrivilege('button', 'Save')"
+          type="primary"
+          icon="el-icon-add"
+          @click="toAdd"
+          >新增</el-button
+        >
+      </div>
+    </div>
+
+    <div class="part-box part-box-pad">
+      <el-table ref="TableList" :data="dataList">
+        <el-table-column
+          type="index"
+          label="序号"
+          width="55"
+          :index="indexMethod"
+        ></el-table-column>
+        <el-table-column prop="outlineName" label="课程大纲" min-width="300">
+        </el-table-column>
+        <el-table-column prop="courseName" label="课程名称" min-width="200">
+        </el-table-column>
+        <el-table-column prop="credit" label="学分" width="80">
+          <template slot-scope="scope">
+            {{ scope.row.credit | defaultFieldFilter }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="evaluationMode" label="考核方式" width="100">
+          <span slot-scope="scope">
+            {{ scope.row.evaluationMode | evaluationModeFilter }}
+          </span>
+        </el-table-column>
+        <el-table-column prop="courseType" label="课程类别" width="160">
+          <span slot-scope="scope">
+            {{ scope.row.courseType | courseTypeFilter }}
+          </span>
+        </el-table-column>
+        <el-table-column prop="semesterName" label="修读学期" width="200">
+        </el-table-column>
+        <el-table-column
+          prop="cultureProgramName"
+          label="所属培养方案"
+          min-width="240"
+        >
+        </el-table-column>
+        <el-table-column
+          prop="targetCount"
+          label="课程目标数量"
+          width="110"
+        ></el-table-column>
+        <el-table-column prop="weightSetting" label="权重设置状态" width="110">
+          <template slot-scope="scope">
+            <span :class="scope.row.weightSetting ? '' : 'color-gray-2'">
+              {{ scope.row.weightSetting ? "已完成" : "未完成" }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          width="180"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              v-if="checkPrivilege('link', 'edit')"
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              v-if="checkPrivilege('link', 'delete')"
+              class="btn-danger"
+              type="text"
+              @click="toDelete(scope.row)"
+              >删除</el-button
+            >
+            <el-button
+              v-if="checkPrivilege('link', 'detail')"
+              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>
+    <!-- ModifyCourseOutline -->
+    <modify-course-outline
+      v-if="checkPrivilege('button', 'Save') || checkPrivilege('link', 'edit')"
+      ref="ModifyCourseOutline"
+      :instance="curRow"
+      @modified="getList"
+    ></modify-course-outline>
+    <!-- DetailCourseOutline -->
+    <detail-course-outline
+      v-if="checkPrivilege('link', 'detail')"
+      ref="DetailCourseOutline"
+      :row-data="curRow"
+      @modified="getList"
+    ></detail-course-outline>
+  </div>
+</template>
+
+<script>
+import { courseOutlineListPage, deleteCourseOutline } from "../api";
+import ModifyCourseOutline from "../components/course-outline/ModifyCourseOutline.vue";
+import DetailCourseOutline from "../components/course-outline/DetailCourseOutline.vue";
+
+export default {
+  name: "course-outline-manage",
+  components: { ModifyCourseOutline, DetailCourseOutline },
+  data() {
+    return {
+      filter: {
+        professionalId: "",
+        cultureProgramId: "",
+        courseId: "",
+        name: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+    };
+  },
+  mounted() {
+    this.toPage(1);
+  },
+  methods: {
+    async getList() {
+      if (!this.checkPrivilege("list", "list")) return;
+
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await courseOutlineListPage(datas);
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    search() {
+      this.toPage(1);
+    },
+    toAdd() {
+      this.curRow = {};
+      this.$refs.ModifyCourseOutline.open();
+    },
+    toEdit(row) {
+      this.curRow = row;
+      this.$refs.ModifyCourseOutline.open();
+    },
+    async toDelete(row) {
+      const confirm = await this.$confirm(
+        `确定要删除课程大纲【${row.outlineName}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await deleteCourseOutline(row.id);
+      this.$message.success("删除成功!");
+      this.deletePageLastItem();
+    },
+    toDetail(row) {
+      this.curRow = row;
+      this.$refs.DetailCourseOutline.open();
+    },
+  },
+};
+</script>

+ 161 - 0
src/modules/target/views/RequirementStatistics.vue

@@ -0,0 +1,161 @@
+<template>
+  <div class="training-plan-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="专业:">
+            <professional-select
+              v-model="filter.professionalId"
+              placeholder="专业"
+              default-select
+              :clearable="false"
+              @default-selected="search"
+            ></professional-select>
+          </el-form-item>
+          <el-form-item label="培养方案名称:">
+            <el-input
+              v-model.trim="filter.cultureProgramName"
+              placeholder="培养方案名称"
+              clearable
+            ></el-input>
+          </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="55"
+          :index="indexMethod"
+        ></el-table-column>
+        <el-table-column prop="obeCultureProgramName" label="培养方案名称">
+        </el-table-column>
+        <el-table-column prop="professionalName" label="专业">
+        </el-table-column>
+        <el-table-column
+          prop="requirementDegree"
+          label="毕业要求达成值"
+          width="120"
+        >
+        </el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          width="160"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              v-if="checkPrivilege('link', 'detail')"
+              class="btn-primary"
+              type="text"
+              @click="toDetail(scope.row)"
+              >查看详情</el-button
+            >
+            <el-button
+              v-if="checkPrivilege('link', 'Calculate')"
+              class="btn-primary"
+              type="text"
+              @click="toCalculate(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>
+    <!-- DetailRequirementStatistics -->
+    <detail-requirement-statistics
+      v-if="checkPrivilege('link', 'detail')"
+      ref="DetailRequirementStatistics"
+      :row-data="curRow"
+    ></detail-requirement-statistics>
+  </div>
+</template>
+
+<script>
+import {
+  requirementStatisticsListPage,
+  requirementStatisticsCalculate,
+} from "../api";
+import DetailRequirementStatistics from "../components/requirement-statistics/DetailRequirementStatistics.vue";
+
+export default {
+  name: "requirement-statistics",
+  components: { DetailRequirementStatistics },
+  data() {
+    return {
+      filter: {
+        professionalId: "",
+        cultureProgramName: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+    };
+  },
+  methods: {
+    async getList() {
+      if (!this.checkPrivilege("list", "list")) return;
+
+      if (!this.filter.professionalId) return;
+
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await requirementStatisticsListPage(datas);
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    search() {
+      this.toPage(1);
+    },
+    toDetail(row) {
+      this.curRow = row;
+      this.$refs.DetailRequirementStatistics.open();
+    },
+    async toCalculate(row) {
+      const confirm = await this.$confirm(`确定要重新计算当前数据吗?`, "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await requirementStatisticsCalculate({
+        cultureProgramId: row.cultureProgramId,
+      });
+      this.getList();
+    },
+  },
+};
+</script>

+ 153 - 0
src/modules/target/views/StudentTarget.vue

@@ -0,0 +1,153 @@
+<template>
+  <div class="student-target">
+    <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="培养方案"
+              :clearable="false"
+              default-select
+              @default-selected="search"
+            ></training-plan-select>
+          </el-form-item>
+          <el-form-item label="学生学号/姓名:">
+            <el-input
+              v-model.trim="filter.studentCodeOrName"
+              placeholder="学生学号/姓名"
+              clearable
+            ></el-input>
+          </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 class="part-box-action">
+        <el-button
+          v-if="checkPrivilege('button', 'Calculate')"
+          type="primary"
+          @click="toCalculate"
+          >重新计算</el-button
+        >
+      </div>
+    </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 prop="studentName" label="姓名"> </el-table-column>
+        <el-table-column prop="studentCode" label="学号"> </el-table-column>
+        <el-table-column prop="collegeName" label="学院"> </el-table-column>
+        <el-table-column prop="majorName" label="专业"> </el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          width="90"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              v-if="checkPrivilege('link', 'Detail')"
+              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>
+    <!-- DetailStudentTarget -->
+    <detail-student-target
+      v-if="checkPrivilege('link', 'Detail')"
+      ref="DetailStudentTarget"
+      :row-data="curRow"
+    ></detail-student-target>
+  </div>
+</template>
+
+<script>
+import { studentTargetListPage, studentTargetCalculate } from "../api";
+import DetailStudentTarget from "../components/student-target/DetailStudentTarget.vue";
+
+export default {
+  name: "student-target",
+  components: { DetailStudentTarget },
+  data() {
+    return {
+      filter: {
+        cultureProgramId: "",
+        studentCodeOrName: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+    };
+  },
+  methods: {
+    async getList() {
+      if (!this.checkPrivilege("list", "list")) return;
+
+      if (!this.filter.cultureProgramId) return;
+
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await studentTargetListPage(datas);
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    search() {
+      this.toPage(1);
+    },
+    toDetail(row) {
+      this.curRow = row;
+      this.$refs.DetailStudentTarget.open();
+    },
+    async toCalculate() {
+      const confirm = await this.$confirm(`确定要重新计算吗?`, "提示", {
+        type: "warning",
+      }).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await studentTargetCalculate({
+        cultureProgramId: this.filter.cultureProgramId,
+      });
+      this.getList();
+    },
+  },
+};
+</script>

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

@@ -0,0 +1,168 @@
+<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="培养方案"
+              :clearable="false"
+              default-select
+              @change="trainingPlanChange"
+              @default-selected="defaultFilterChange('cultureProgramId')"
+            ></training-plan-select>
+          </el-form-item>
+          <el-form-item label="学期:">
+            <semester-select
+              v-model="filter.semesterId"
+              placeholder="学期"
+              :clearable="false"
+              style="width: 220px"
+              default-select
+              @default-selected="defaultFilterChange('semesterId')"
+            ></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="课程(代码)" min-width="300">
+          <template slot-scope="scope">
+            {{ scope.row.courseName | defaultFieldFilter }}({{
+              scope.row.courseCode | defaultFieldFilter
+            }})
+          </template>
+        </el-table-column>
+        <el-table-column prop="userName" label="创建人" min-width="200">
+          <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: {},
+      defaultKeys: [],
+    };
+  },
+  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() {
+      if (!this.filter.cultureProgramId || !this.filter.semesterId) {
+        this.$message.error("培养方案和学期必选!");
+        return;
+      }
+
+      this.toPage(1);
+    },
+    trainingPlanChange(val) {
+      this.filter.professionalId = val?.professionalId;
+    },
+    defaultFilterChange(key) {
+      if (!this.defaultKeys.includes(key)) {
+        this.defaultKeys.push(key);
+      }
+      if (this.defaultKeys.length === 2) this.search();
+    },
+    toDetail(row) {
+      this.curRow = row;
+      this.$refs.DetailTargetScore.open();
+    },
+  },
+};
+</script>

+ 157 - 0
src/modules/target/views/TargetStatistics.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="target-statistics">
+    <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="培养方案"
+              :clearable="false"
+              default-select
+              @default-selected="search"
+              @change="trainingPlanChange"
+            ></training-plan-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"
+              :clearable="false"
+            ></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="课程(代码)" min-width="240">
+          <template slot-scope="scope">
+            {{ scope.row.courseName | defaultFieldFilter }}({{
+              scope.row.courseCode | defaultFieldFilter
+            }})
+          </template>
+        </el-table-column>
+        <el-table-column prop="semesterName" label="修读学期" width="200">
+        </el-table-column>
+        <el-table-column
+          prop="cultureProgramName"
+          label="所属培养方案"
+          min-width="260"
+        >
+        </el-table-column>
+        <el-table-column prop="userName" label="创建人" min-width="200">
+          <span slot-scope="scope">
+            {{ scope.row.userName }}({{ scope.row.userLoginName }})
+          </span>
+        </el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          width="90"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              v-if="checkPrivilege('link', 'view')"
+              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>
+    <!-- DetailTargetStatistics -->
+    <detail-target-statistics
+      v-if="checkPrivilege('link', 'view')"
+      ref="DetailTargetStatistics"
+      :course="curRow"
+    ></detail-target-statistics>
+  </div>
+</template>
+
+<script>
+import { targetStatisticsListPage } from "../api";
+import DetailTargetStatistics from "../components/target-statistics/DetailTargetStatistics.vue";
+
+export default {
+  name: "target-statistics",
+  components: { DetailTargetStatistics },
+  data() {
+    return {
+      filter: {
+        professionalId: "",
+        cultureProgramId: "",
+        courseId: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+    };
+  },
+  methods: {
+    async getList() {
+      if (!this.checkPrivilege("list", "list")) return;
+
+      if (!this.filter.cultureProgramId) return;
+
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await targetStatisticsListPage(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.DetailTargetStatistics.open();
+    },
+  },
+};
+</script>

+ 225 - 0
src/modules/target/views/TrainingPlanManage.vue

@@ -0,0 +1,225 @@
+<template>
+  <div class="training-plan-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="专业:">
+            <professional-select
+              v-model="filter.professionalId"
+              placeholder="专业"
+            ></professional-select>
+          </el-form-item>
+          <el-form-item label="培养方案名称:">
+            <el-input
+              v-model.trim="filter.name"
+              placeholder="培养方案名称"
+              clearable
+            ></el-input>
+          </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 class="part-box-action">
+        <el-button
+          v-if="checkPrivilege('button', 'add')"
+          type="primary"
+          icon="el-icon-add"
+          @click="toAdd"
+          >新增</el-button
+        >
+      </div>
+    </div>
+
+    <div class="part-box part-box-pad">
+      <el-table ref="TableList" :data="dataList">
+        <el-table-column
+          type="index"
+          label="序号"
+          width="55"
+          :index="indexMethod"
+        ></el-table-column>
+        <el-table-column prop="name" label="培养方案名称" min-width="300">
+        </el-table-column>
+        <el-table-column prop="professionalName" label="专业" min-width="200">
+        </el-table-column>
+        <el-table-column prop="targetCount" label="培养目标" width="80">
+          <span slot-scope="scope"> {{ scope.row.targetCount }}个 </span>
+        </el-table-column>
+        <el-table-column prop="requirementCount" label="毕业要求" width="80">
+          <span slot-scope="scope"> {{ scope.row.requirementCount }}项 </span>
+        </el-table-column>
+        <el-table-column prop="courseCount" label="课程体系" width="80">
+          <span slot-scope="scope"> {{ scope.row.courseCount }}门 </span>
+        </el-table-column>
+        <el-table-column prop="createName" label="创建人" min-width="200">
+          <span slot-scope="scope">
+            {{ scope.row.createRealName }}({{ scope.row.createLoginName }})
+          </span>
+        </el-table-column>
+        <el-table-column
+          class-name="action-column"
+          label="操作"
+          width="220"
+          fixed="right"
+        >
+          <template slot-scope="scope">
+            <el-button
+              v-if="checkPrivilege('link', 'edit')"
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              v-if="checkPrivilege('link', 'copy')"
+              class="btn-primary"
+              type="text"
+              @click="toCopy(scope.row)"
+              >复制</el-button
+            >
+            <el-button
+              v-if="checkPrivilege('link', 'delete')"
+              class="btn-danger"
+              type="text"
+              @click="toDelete(scope.row)"
+              >删除</el-button
+            >
+            <el-button
+              v-if="checkPrivilege('link', 'CultureProgramDetail')"
+              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>
+    <!-- ModifyTrainingPlan -->
+    <modify-training-plan
+      v-if="checkPrivilege('button', 'add') || checkPrivilege('link', 'edit')"
+      ref="ModifyTrainingPlan"
+      :instance="curRow"
+      @modified="getList"
+    ></modify-training-plan>
+    <!-- DetailTrainingPlan -->
+    <detail-training-plan
+      v-if="checkPrivilege('link', 'CultureProgramDetail')"
+      ref="DetailTrainingPlan"
+      :row-data="curRow"
+      @modified="getList"
+    ></detail-training-plan>
+  </div>
+</template>
+
+<script>
+import {
+  trainingPlanListPage,
+  deleteTrainingPlan,
+  copyTrainingPlan,
+} from "../api";
+import ModifyTrainingPlan from "../components/training-plan/ModifyTrainingPlan.vue";
+import DetailTrainingPlan from "../components/training-plan/DetailTrainingPlan.vue";
+
+export default {
+  name: "training-plan-manage",
+  components: { ModifyTrainingPlan, DetailTrainingPlan },
+  data() {
+    return {
+      filter: {
+        professionalId: "",
+        name: "",
+      },
+      current: 1,
+      size: this.GLOBAL.pageSize,
+      total: 0,
+      dataList: [],
+      curRow: {},
+    };
+  },
+  mounted() {
+    this.toPage(1);
+  },
+  methods: {
+    async getList() {
+      if (!this.checkPrivilege("list", "list")) return;
+
+      const datas = {
+        ...this.filter,
+        pageNumber: this.current,
+        pageSize: this.size,
+      };
+      const data = await trainingPlanListPage(datas);
+      this.dataList = data.records;
+      this.total = data.total;
+    },
+    toPage(page) {
+      this.current = page;
+      this.getList();
+    },
+    search() {
+      this.toPage(1);
+    },
+    toAdd() {
+      this.curRow = {};
+      this.$refs.ModifyTrainingPlan.open();
+    },
+    toEdit(row) {
+      this.curRow = row;
+      this.$refs.ModifyTrainingPlan.open();
+    },
+    async toDelete(row) {
+      const confirm = await this.$confirm(
+        `确定要删除培养方案【${row.name}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await deleteTrainingPlan(row.id);
+      this.$message.success("删除成功!");
+      this.deletePageLastItem();
+    },
+    async toCopy(row) {
+      const confirm = await this.$confirm(
+        `确定要复制培养方案【${row.name}】吗?`,
+        "提示",
+        {
+          type: "warning",
+        }
+      ).catch(() => {});
+      if (confirm !== "confirm") return;
+
+      await copyTrainingPlan(row.id);
+      this.$message.success("复制成功!");
+      this.getList();
+    },
+    toDetail(row) {
+      this.curRow = row;
+      this.$refs.DetailTrainingPlan.open();
+    },
+  },
+};
+</script>

+ 2 - 2
src/plugins/VueCharts.js

@@ -13,7 +13,7 @@ import {
   LineChart,
   // PieChart,
   // ScatterChart,
-  // RadarChart,
+  RadarChart,
   BoxplotChart,
 } from "echarts/charts";
 
@@ -34,7 +34,7 @@ use([
   LineChart,
   // PieChart,
   // ScatterChart,
-  // RadarChart,
+  RadarChart,
   BoxplotChart,
   GridComponent,
   TitleComponent,

+ 9 - 1
src/plugins/filters.js

@@ -26,13 +26,15 @@ import {
   FLOW_TYPE,
   MARK_MODE_TYPE,
   STUDENT_STATUS,
+  EVALUATION_MODE,
+  COURSE_TYPE,
 } from "../constants/enumerate";
 import { formatDate } from "../plugins/utils";
 
 const DEFAULT_FIELD = "--";
 
 Vue.filter("defaultFieldFilter", function (val) {
-  return val || DEFAULT_FIELD;
+  return val || val === 0 ? val : DEFAULT_FIELD;
 });
 Vue.filter("enableFilter", function (val) {
   return val ? "启用" : "禁用";
@@ -132,3 +134,9 @@ Vue.filter("markModeTypeFilter", function (val) {
 Vue.filter("studentStatusFilter", function (val) {
   return STUDENT_STATUS[val] || DEFAULT_FIELD;
 });
+Vue.filter("evaluationModeFilter", function (val) {
+  return EVALUATION_MODE[val] || DEFAULT_FIELD;
+});
+Vue.filter("courseTypeFilter", function (val) {
+  return COURSE_TYPE[val] || DEFAULT_FIELD;
+});

+ 6 - 0
src/plugins/globalVuePlugins.js

@@ -29,6 +29,9 @@ import ExamSelect from "../components/base/ExamSelect.vue";
 import OrgSelect from "../components/base/OrgSelect.vue";
 import DataTaskDialog from "../components/base/DataTaskDialog.vue";
 import StatusSelect from "../components/base/StatusSelect.vue";
+import ProfessionalSelect from "../components/base/ProfessionalSelect.vue";
+import TrainingPlanSelect from "../components/base/TrainingPlanSelect.vue";
+import TrainingPlanCourseSelect from "../components/base/TrainingPlanCourseSelect.vue";
 // base
 import BaseCourseSelect from "../components/base/BaseCourseSelect.vue";
 // other
@@ -61,6 +64,9 @@ const components = {
   OrgSelect,
   DataTaskDialog,
   StatusSelect,
+  ProfessionalSelect,
+  TrainingPlanSelect,
+  TrainingPlanCourseSelect,
   // base
   BaseCourseSelect,
   // other

+ 44 - 9
src/plugins/utils.js

@@ -164,7 +164,7 @@ export function formatDate(format = "YYYY/MM/DD HH:mm:ss", date = new Date()) {
   Object.entries(options).map(([key, val]) => {
     if (new RegExp("(" + key + ")").test(format)) {
       const zeros = key === "Y+" ? "0000" : "00";
-      const value = (zeros + val).substr(("" + val).length);
+      const value = (zeros + val).substring(("" + val).length);
       format = format.replace(RegExp.$1, value);
     }
   });
@@ -339,6 +339,11 @@ export function maxNum(dataList) {
   if (!dataList.length) return 0;
   return Math.max.apply(null, dataList);
 }
+/** 获取数组最小数 */
+export function minNum(dataList) {
+  if (!dataList.length) return 0;
+  return Math.min.apply(null, dataList);
+}
 
 export function isEmptyObject(obj) {
   return !Object.keys(obj).length;
@@ -452,13 +457,43 @@ export function numberToChinese(num) {
 }
 
 /**
- * 移除富文本json字符串中的type=text的value值
- * @param {string} data 富文本json字符串
- * @returns 字符串
+ * 将给定的字符串按设置的字符个数分段
+ * @param {string} content 字符串内容
+ * @param {number} countPerGroup 每段字符最大个数
+ * @returns 分段数组
+ */
+export function splitContent(content, countPerGroup) {
+  if (!countPerGroup) return content;
+  const gCount = Math.ceil(content.length / countPerGroup);
+  const cpg = Math.ceil(content.length / gCount);
+
+  const groups = [];
+  for (let i = 0; i < gCount; i++) {
+    groups.push(content.substring(i * cpg, (i + 1) * cpg));
+  }
+  return groups;
+}
+
+/**
+ * 获取远程json文件的内容
+ * @param {string} url json文件远程地址
  */
-export function removeRichTextValue(data) {
-  return data.replace(
-    /"type":"text","value":"(.*?)"/g,
-    '"type":"text","value":""'
-  );
+export async function getJsonDataFormUrl(url) {
+  const response = await fetch(url);
+  if (!response.ok) {
+    throw new Error(`fetch error! response.status: ${response.status}`);
+  }
+  const data = await response.json();
+  return data;
+}
+
+export function toPrecision(num, fractionDigits) {
+  if (fractionDigits < 0 || fractionDigits > 16) return num;
+
+  const tnum = Math.pow(10, fractionDigits);
+  return Math.round(num * tnum) / tnum;
+}
+
+export function listFilterEmpty(list) {
+  return list.filter((item) => item || item === 0);
 }

+ 2 - 0
src/router.js

@@ -14,6 +14,7 @@ import stmms from "./modules/stmms/router";
 import analysis from "./modules/analysis/router";
 import mark from "./modules/mark/router";
 import course from "./modules/course/router";
+import target from "./modules/target/router";
 // card part
 import card from "./modules/card/router";
 // admin
@@ -68,6 +69,7 @@ let router = new Router({
         ...analysis,
         ...mark,
         ...course,
+        ...target,
       ],
     },
     { ...login },

+ 2 - 0
src/store.js

@@ -14,6 +14,7 @@ import base from "./modules/base/store";
 import baseConfigs from "./modules/analysis/components/baseConfig/baseConfigsStore";
 import markParam from "./modules/mark/components/markParam/store";
 import report from "./modules/mark/components/report/store";
+import target from "./modules/target/store";
 
 export default new Vuex.Store({
   state: {
@@ -38,5 +39,6 @@ export default new Vuex.Store({
     baseConfigs,
     markParam,
     report,
+    target,
   },
 });

+ 1 - 1
vue.config.js

@@ -9,7 +9,7 @@ let config = {
   transpileDependencies: true,
   // publicPath: './',
   devServer: {
-    port: 8056,
+    port: process.env.VUE_APP_DEV_PORT || 8056,
     proxy: {
       "/api/": {
         target: process.env.VUE_APP_DEV_PROXY,

+ 1 - 1
yarn.lock

@@ -3942,7 +3942,7 @@ json5@^1.0.1:
   dependencies:
     minimist "^1.2.0"
 
-json5@^2.1.2, json5@^2.2.2:
+json5@^2.1.2, json5@^2.2.2, json5@^2.2.3:
   version "2.2.3"
   resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
   integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==