Przeglądaj źródła

Merge remote-tracking branch 'origin/master' into feature/dlwork

# Conflicts:
#	vue.config.js
Prevalence 7 lat temu
rodzic
commit
1b7440fa3a
36 zmienionych plików z 1898 dodań i 746 usunięć
  1. 227 227
      package-lock.json
  2. 3 1
      package.json
  3. 35 3
      src/api/codehw.js
  4. 20 0
      src/api/resultstatistics.js
  5. 22 2
      src/components/CheckList/index.vue
  6. 21 0
      src/components/CodeStateTag/index.vue
  7. 1 1
      src/components/DocumentStateTag/index.vue
  8. 226 0
      src/components/NoDataSvg.vue
  9. 23 0
      src/data/codeState.js
  10. 6 6
      src/layouts/courseLayouts/StudentMenu.vue
  11. 5 5
      src/layouts/courseLayouts/TeacherMenu.vue
  12. 3 3
      src/router/routes.js
  13. 69 0
      src/util/exportExcel.js
  14. 54 4
      src/views/student/Code/ProjectDetail/BuildDetail.vue
  15. 265 22
      src/views/student/Code/ProjectDetail/DeployDetail.vue
  16. 15 6
      src/views/student/Code/ProjectDetail/Test/FunctionalTestDetail.vue
  17. 4 2
      src/views/student/Code/ProjectDetail/Test/UnitTestDetail.vue
  18. 30 22
      src/views/student/Code/ProjectDetail/TestDetail.vue
  19. 2 1
      src/views/student/Code/ProjectDetail/index.vue
  20. 17 2
      src/views/student/Code/index.vue
  21. 7 1
      src/views/student/Document/index.vue
  22. 1 1
      src/views/student/Home/index.vue
  23. 7 2
      src/views/student/Review/ReviewDetail/ReviewSelfDetail/index.vue
  24. 11 7
      src/views/student/Review/ReviewDocDetail/index.vue
  25. 7 1
      src/views/student/Review/index.vue
  26. 118 0
      src/views/teacher/Code/ProjectDetail/BuildDetail.vue
  27. 172 0
      src/views/teacher/Code/ProjectDetail/DeployDetail.vue
  28. 156 0
      src/views/teacher/Code/ProjectDetail/TestDetail.vue
  29. 8 2
      src/views/teacher/Code/index.vue
  30. 7 1
      src/views/teacher/Document/index.vue
  31. 1 1
      src/views/teacher/Home/index.vue
  32. 5 256
      src/views/teacher/Question/index.vue
  33. 260 160
      src/views/teacher/Result/index.vue
  34. 1 1
      src/views/teacher/Review/ReviewDetail/GroupArgueDetail/index.vue
  35. 19 1
      src/views/teacher/Review/index.vue
  36. 70 5
      yarn.lock

Plik diff jest za duży
+ 227 - 227
package-lock.json


+ 3 - 1
package.json

@@ -14,6 +14,7 @@
     "buefy": "^0.7.1",
     "chart.js": "^2.7.3",
     "dayjs": "^1.7.8",
+    "file-saver": "^2.0.1",
     "register-service-worker": "^1.5.2",
     "vue": "^2.5.17",
     "vue-chartjs": "^3.4.0",
@@ -22,7 +23,8 @@
     "vue-router": "^3.0.1",
     "vuelidate": "^0.7.4",
     "vuex": "^3.0.1",
-    "whatwg-fetch": "^3.0.0"
+    "whatwg-fetch": "^3.0.0",
+    "xlsx": "^0.14.1"
   },
   "devDependencies": {
     "@pollyjs/adapter-fetch": "^1.4.1",

+ 35 - 3
src/api/codehw.js

@@ -50,6 +50,28 @@ export const getBuildDetail = buildId => {
   return request(`${CODE_HOMEWORK_MODULE}/build/${buildId}`);
 };
 
+/**
+ * 获得最近n个构建的信息
+ * @param {string|number} projectId
+ * @return {Promise<{id:string,data:BuildSerializerType[]}>}
+ */
+export const getLatestBuildInfoList = (projectId, limit = 5) => {
+  return request(
+    `${CODE_HOMEWORK_MODULE}/project/${projectId}/build/record?limit=${limit}`
+  );
+};
+
+/**
+ * 获得最近n个部署的信息
+ * @param {string|number} projectId
+ * @return {Promise<{id:string,data:DeploySerializerType[]}>}
+ */
+export const getLatestDeployInfoList = (projectId, limit = 5) => {
+  return request(
+    `${CODE_HOMEWORK_MODULE}/project/deploy/record/${projectId}?limit=${limit}`
+  );
+};
+
 /**
  * 获得一次部署的详细信息
  * @param {string|number} deployId
@@ -59,6 +81,15 @@ export const getDeployDetail = deployId => {
   return request(`${CODE_HOMEWORK_MODULE}/deploy/${deployId}`);
 };
 
+/**
+ * 获得一次部署的日志信息
+ * @param {string|number} deployId
+ * @return {Promise<{id:string,data:string}>}
+ */
+export const getDeployLogInfo = deployId => {
+  return request(`${CODE_HOMEWORK_MODULE}/deploy/log/${deployId}`);
+};
+
 /**
  * 获得一次单元测试的详细信息
  * @param {string|number} unitId
@@ -90,15 +121,16 @@ export const getAvailableMirrorList = ({ homeworkId, projectId }) => {
 /**
  * TODO: arguments 不被允许
  * 触发一次部署
- * @param {string|number} homeworkId
+ * @param {string|number} replicas
  * @param {string|number} projectId
  * @param {string|number} mirrorId
  * @return {Promise<{data:{success:boolean}}>}
  */
-export const makeDeploy = ({ homeworkId, projectId, mirrorId }) => {
+export const makeDeploy = ({ projectId, mirrorId, replicas }) => {
+  console.log(mirrorId);
   return request(`${CODE_HOMEWORK_MODULE}/deploy`, {
     method: "POST",
-    body: { homeworkId, projectId, mirrorId }
+    body: { replicas, projectId, mirrorId }
   });
 };
 

+ 20 - 0
src/api/resultstatistics.js

@@ -10,3 +10,23 @@ export const getStudentFinaleResult = (courseId = 123) => {
     method: "POST"
   });
 };
+
+/**
+ * 设置作业成绩比例
+ * @returns {Promise<{data:string}>}
+ */
+export const setResultPercentList = (
+  courseId,
+  docPercentList,
+  codePercentList,
+  reviewPercentList
+) => {
+  return request(`${RESULTSTATISTICS_MODULE}/course/${courseId}/percent`, {
+    method: "POST",
+    body: {
+      documents: docPercentList,
+      codes: codePercentList,
+      reviews: reviewPercentList
+    }
+  });
+};

+ 22 - 2
src/components/CheckList/index.vue

@@ -11,7 +11,7 @@
           class="checkbox-section"
           v-model="item.level"
           :type="item.state === 'SOLVING' ? 'is-primary' : 'is-success'"
-          :disabled="item.state === 'PENDING' || item.state === 'ARGUED'"
+          :disabled="judgeDisable(item)"
         >
           <b-tooltip :label="item.explain" position="is-top">
             {{ item.content }}
@@ -37,7 +37,7 @@
           'input-custom--warning': item.state === 'PENDING',
           'input-custom--end': item.state === 'ARGUED'
         }"
-        :disabled="item.state === 'PENDING' || item.state === 'ARGUED'"
+        :disabled="judgeDisable(item)"
         type="text"
         placeholder="填写备注"
         style="box-shadow:none;border-color:white"
@@ -83,6 +83,16 @@ export default {
     value: {
       type: Array,
       default: () => []
+    },
+    review: {
+      type: Object,
+      default: () => ({
+        state: "DEFAULT"
+      })
+    },
+    isTeacher: {
+      type: Boolean,
+      default: false
     }
   },
   data() {
@@ -101,6 +111,16 @@ export default {
     }
   },
   methods: {
+    judgeDisable(item) {
+      if (this.isTeacher) {
+        return false;
+      }
+      return (
+        item.state === "PENDING" ||
+        item.state === "ARGUED" ||
+        this.review.state === "ARGUING"
+      );
+    },
     async acceptArgue(item) {
       this.acceptArgueLoading = true;
       await this.$emit("accept-argue", item);

+ 21 - 0
src/components/CodeStateTag/index.vue

@@ -0,0 +1,21 @@
+<template>
+  <div class="tag" :class="tagDetail.styledClass">{{ tagDetail.name }}</div>
+</template>
+
+<script>
+import getCodeState from "../../data/codeState";
+export default {
+  name: "DocumentStateTag",
+  props: {
+    state: {
+      type: String,
+      default: "已失效"
+    }
+  },
+  computed: {
+    tagDetail() {
+      return getCodeState(this.state);
+    }
+  }
+};
+</script>

+ 1 - 1
src/components/DocumentStateTag/index.vue

@@ -9,7 +9,7 @@ export default {
   props: {
     state: {
       type: String,
-      default: "已失效"
+      default: "无状态"
     }
   },
   computed: {

+ 226 - 0
src/components/NoDataSvg.vue

@@ -0,0 +1,226 @@
+<template>
+  <!--当没有数据时页面展示-->
+  <div class="no-data">
+    <svg
+      id="f20e0c25-d928-42cc-98d1-13cc230663ea"
+      data-name="Layer 1"
+      xmlns="http://www.w3.org/2000/svg"
+      xmlns:xlink="http://www.w3.org/1999/xlink"
+      width="620.16"
+      height="500.81"
+      viewBox="0 0 820.16 780.81"
+    >
+      <defs>
+        <linearGradient
+          id="07332201-7176-49c2-9908-6dc4a39c4716"
+          x1="539.63"
+          y1="734.6"
+          x2="539.63"
+          y2="151.19"
+          gradientTransform="translate(-3.62 1.57)"
+          gradientUnits="userSpaceOnUse"
+        >
+          <stop offset="0" stop-color="gray" stop-opacity="0.25" />
+          <stop offset="0.54" stop-color="gray" stop-opacity="0.12" />
+          <stop offset="1" stop-color="gray" stop-opacity="0.1" />
+        </linearGradient>
+        <linearGradient
+          id="0ee1ab3f-7ba2-4205-9d4a-9606ad702253"
+          x1="540.17"
+          y1="180.2"
+          x2="540.17"
+          y2="130.75"
+          gradientTransform="translate(-63.92 7.85)"
+          xlink:href="#07332201-7176-49c2-9908-6dc4a39c4716"
+        />
+        <linearGradient
+          id="abca9755-bed1-4a97-b027-7f02ee3ffa09"
+          x1="540.17"
+          y1="140.86"
+          x2="540.17"
+          y2="82.43"
+          gradientTransform="translate(-84.51 124.6) rotate(-12.11)"
+          xlink:href="#07332201-7176-49c2-9908-6dc4a39c4716"
+        />
+        <linearGradient
+          id="2632d424-e666-4ee4-9508-a494957e14ab"
+          x1="476.4"
+          y1="710.53"
+          x2="476.4"
+          y2="127.12"
+          gradientTransform="matrix(1, 0, 0, 1, 0, 0)"
+          xlink:href="#07332201-7176-49c2-9908-6dc4a39c4716"
+        />
+        <linearGradient
+          id="97571ef7-1c83-4e06-b701-c2e47e77dca3"
+          x1="476.94"
+          y1="156.13"
+          x2="476.94"
+          y2="106.68"
+          gradientTransform="matrix(1, 0, 0, 1, 0, 0)"
+          xlink:href="#07332201-7176-49c2-9908-6dc4a39c4716"
+        />
+        <linearGradient
+          id="7d32e13e-a0c7-49c4-af0e-066a2f8cb76e"
+          x1="666.86"
+          y1="176.39"
+          x2="666.86"
+          y2="117.95"
+          gradientTransform="matrix(1, 0, 0, 1, 0, 0)"
+          xlink:href="#07332201-7176-49c2-9908-6dc4a39c4716"
+        />
+      </defs>
+      <title>no data</title>
+      <rect
+        x="317.5"
+        y="142.55"
+        width="437.02"
+        height="603.82"
+        transform="translate(-271.22 62.72) rotate(-12.11)"
+        fill="#e0e0e0"
+      />
+      <g opacity="0.5">
+        <rect
+          x="324.89"
+          y="152.76"
+          width="422.25"
+          height="583.41"
+          transform="translate(-271.22 62.72) rotate(-12.11)"
+          fill="url(#07332201-7176-49c2-9908-6dc4a39c4716)"
+        />
+      </g>
+      <rect
+        x="329.81"
+        y="157.1"
+        width="411.5"
+        height="570.52"
+        transform="translate(-270.79 62.58) rotate(-12.11)"
+        fill="#fafafa"
+      />
+      <rect
+        x="374.18"
+        y="138.6"
+        width="204.14"
+        height="49.45"
+        transform="translate(-213.58 43.93) rotate(-12.11)"
+        fill="url(#0ee1ab3f-7ba2-4205-9d4a-9606ad702253)"
+      />
+      <path
+        d="M460.93,91.9c-15.41,3.31-25.16,18.78-21.77,34.55s18.62,25.89,34,22.58,25.16-18.78,21.77-34.55S476.34,88.59,460.93,91.9ZM470.6,137A16.86,16.86,0,1,1,483.16,117,16.66,16.66,0,0,1,470.6,137Z"
+        transform="translate(-189.92 -59.59)"
+        fill="url(#abca9755-bed1-4a97-b027-7f02ee3ffa09)"
+      />
+      <rect
+        x="375.66"
+        y="136.55"
+        width="199.84"
+        height="47.27"
+        transform="translate(-212.94 43.72) rotate(-12.11)"
+        fill="#6c63ff"
+      />
+      <path
+        d="M460.93,91.9a27.93,27.93,0,1,0,33.17,21.45A27.93,27.93,0,0,0,460.93,91.9ZM470.17,135a16.12,16.12,0,1,1,12.38-19.14A16.12,16.12,0,0,1,470.17,135Z"
+        transform="translate(-189.92 -59.59)"
+        fill="#6c63ff"
+      />
+      <rect
+        x="257.89"
+        y="116.91"
+        width="437.02"
+        height="603.82"
+        fill="#e0e0e0"
+      />
+      <g opacity="0.5">
+        <rect
+          x="265.28"
+          y="127.12"
+          width="422.25"
+          height="583.41"
+          fill="url(#2632d424-e666-4ee4-9508-a494957e14ab)"
+        />
+      </g>
+      <rect x="270.65" y="131.42" width="411.5" height="570.52" fill="#fff" />
+      <rect
+        x="374.87"
+        y="106.68"
+        width="204.14"
+        height="49.45"
+        fill="url(#97571ef7-1c83-4e06-b701-c2e47e77dca3)"
+      />
+      <path
+        d="M666.86,118c-15.76,0-28.54,13.08-28.54,29.22s12.78,29.22,28.54,29.22,28.54-13.08,28.54-29.22S682.62,118,666.86,118Zm0,46.08a16.86,16.86,0,1,1,16.46-16.86A16.66,16.66,0,0,1,666.86,164Z"
+        transform="translate(-189.92 -59.59)"
+        fill="url(#7d32e13e-a0c7-49c4-af0e-066a2f8cb76e)"
+      />
+      <rect
+        x="377.02"
+        y="104.56"
+        width="199.84"
+        height="47.27"
+        fill="#6c63ff"
+      />
+      <path
+        d="M666.86,118a27.93,27.93,0,1,0,27.93,27.93A27.93,27.93,0,0,0,666.86,118Zm0,44.05A16.12,16.12,0,1,1,683,145.89,16.12,16.12,0,0,1,666.86,162Z"
+        transform="translate(-189.92 -59.59)"
+        fill="#6c63ff"
+      />
+      <g opacity="0.5">
+        <rect x="15.27" y="737.05" width="3.76" height="21.33" fill="#47e6b1" />
+        <rect
+          x="205.19"
+          y="796.65"
+          width="3.76"
+          height="21.33"
+          transform="translate(824.47 540.65) rotate(90)"
+          fill="#47e6b1"
+        />
+      </g>
+      <g opacity="0.5">
+        <rect x="451.49" width="3.76" height="21.33" fill="#47e6b1" />
+        <rect
+          x="641.4"
+          y="59.59"
+          width="3.76"
+          height="21.33"
+          transform="translate(523.63 -632.62) rotate(90)"
+          fill="#47e6b1"
+        />
+      </g>
+      <path
+        d="M961,832.15a4.61,4.61,0,0,1-2.57-5.57,2.22,2.22,0,0,0,.1-.51h0a2.31,2.31,0,0,0-4.15-1.53h0a2.22,2.22,0,0,0-.26.45,4.61,4.61,0,0,1-5.57,2.57,2.22,2.22,0,0,0-.51-.1h0a2.31,2.31,0,0,0-1.53,4.15h0a2.22,2.22,0,0,0,.45.26,4.61,4.61,0,0,1,2.57,5.57,2.22,2.22,0,0,0-.1.51h0a2.31,2.31,0,0,0,4.15,1.53h0a2.22,2.22,0,0,0,.26-.45,4.61,4.61,0,0,1,5.57-2.57,2.22,2.22,0,0,0,.51.1h0a2.31,2.31,0,0,0,1.53-4.15h0A2.22,2.22,0,0,0,961,832.15Z"
+        transform="translate(-189.92 -59.59)"
+        fill="#4d8af0"
+        opacity="0.5"
+      />
+      <path
+        d="M326.59,627.09a4.61,4.61,0,0,1-2.57-5.57,2.22,2.22,0,0,0,.1-.51h0a2.31,2.31,0,0,0-4.15-1.53h0a2.22,2.22,0,0,0-.26.45,4.61,4.61,0,0,1-5.57,2.57,2.22,2.22,0,0,0-.51-.1h0a2.31,2.31,0,0,0-1.53,4.15h0a2.22,2.22,0,0,0,.45.26,4.61,4.61,0,0,1,2.57,5.57,2.22,2.22,0,0,0-.1.51h0a2.31,2.31,0,0,0,4.15,1.53h0a2.22,2.22,0,0,0,.26-.45A4.61,4.61,0,0,1,325,631.4a2.22,2.22,0,0,0,.51.1h0a2.31,2.31,0,0,0,1.53-4.15h0A2.22,2.22,0,0,0,326.59,627.09Z"
+        transform="translate(-189.92 -59.59)"
+        fill="#fdd835"
+        opacity="0.5"
+      />
+      <path
+        d="M855,127.77a4.61,4.61,0,0,1-2.57-5.57,2.22,2.22,0,0,0,.1-.51h0a2.31,2.31,0,0,0-4.15-1.53h0a2.22,2.22,0,0,0-.26.45,4.61,4.61,0,0,1-5.57,2.57,2.22,2.22,0,0,0-.51-.1h0a2.31,2.31,0,0,0-1.53,4.15h0a2.22,2.22,0,0,0,.45.26,4.61,4.61,0,0,1,2.57,5.57,2.22,2.22,0,0,0-.1.51h0a2.31,2.31,0,0,0,4.15,1.53h0a2.22,2.22,0,0,0,.26-.45,4.61,4.61,0,0,1,5.57-2.57,2.22,2.22,0,0,0,.51.1h0a2.31,2.31,0,0,0,1.53-4.15h0A2.22,2.22,0,0,0,855,127.77Z"
+        transform="translate(-189.92 -59.59)"
+        fill="#fdd835"
+        opacity="0.5"
+      />
+      <circle cx="812.64" cy="314.47" r="7.53" fill="#f55f44" opacity="0.5" />
+      <circle cx="230.73" cy="746.65" r="7.53" fill="#f55f44" opacity="0.5" />
+      <circle cx="735.31" cy="477.23" r="7.53" fill="#f55f44" opacity="0.5" />
+      <circle cx="87.14" cy="96.35" r="7.53" fill="#4d8af0" opacity="0.5" />
+      <circle cx="7.53" cy="301.76" r="7.53" fill="#47e6b1" opacity="0.5" />
+    </svg>
+    <span>暂无相关数据</span>
+  </div>
+</template>
+<style lang="scss" scoped>
+.no-data {
+  display: flex;
+  align-items: center;
+  span {
+    font-size: 2rem;
+    font-weight: bold;
+    color: #6c63ff;
+  }
+}
+</style>

+ 23 - 0
src/data/codeState.js

@@ -0,0 +1,23 @@
+export const DEFAULT = "DEFAULT";
+export const PREPARING = "PREPARING";
+export const UNAVAILABLE = "UNAVAILABLE";
+export const READY = "READY";
+export const AVAILABLE = "AVAILABLE";
+export const FINISHED = "FINISHED";
+
+export default function(state) {
+  switch (state) {
+    case PREPARING:
+      return { name: "准备中", styledClass: "is-info" };
+    case UNAVAILABLE:
+      return { name: "创建失败", styledClass: "is-warning" };
+    case READY:
+      return { name: "未开始", styledClass: "is-light" };
+    case AVAILABLE:
+      return { name: "正在进行", styledClass: "is-success" };
+    case FINISHED:
+      return { name: "已结束", styledClass: "is-danger" };
+    default:
+      return { name: "无状态", styledClass: "is-dark" };
+  }
+}

+ 6 - 6
src/layouts/courseLayouts/StudentMenu.vue

@@ -1,12 +1,12 @@
 <template>
   <aside class="menu dark-menu">
-    <p class="menu-label">总览</p>
+    <!--<p class="menu-label">总览</p>-->
     <ul class="menu-list">
-      <li>
-        <router-link active-class="is-active" :to="`${prefix}/dashboard`">
-          <img alt="" src="../../assets/icon/work.png" />工作台
-        </router-link>
-      </li>
+      <!--<li>-->
+      <!--<router-link active-class="is-active" :to="`${prefix}/dashboard`">-->
+      <!--<img alt="" src="../../assets/icon/work.png" />工作台-->
+      <!--</router-link>-->
+      <!--</li>-->
     </ul>
     <p class="menu-label">作业</p>
     <ul class="menu-list">

+ 5 - 5
src/layouts/courseLayouts/TeacherMenu.vue

@@ -2,11 +2,11 @@
   <aside class="menu dark-menu">
     <p class="menu-label">总览</p>
     <ul class="menu-list">
-      <li>
-        <router-link active-class="is-active" :to="`${prefix}/dashboard`">
-          <img alt="" src="../../assets/icon/work.png" />工作台
-        </router-link>
-      </li>
+      <!--<li>-->
+      <!--<router-link active-class="is-active" :to="`${prefix}/dashboard`">-->
+      <!--<img alt="" src="../../assets/icon/work.png" />工作台-->
+      <!--</router-link>-->
+      <!--</li>-->
       <li>
         <router-link active-class="is-active" :to="`/teacher/`">
           <img alt="" src="../../assets/icon/home.png" />课程选择

+ 3 - 3
src/router/routes.js

@@ -91,17 +91,17 @@ export default [
           {
             path: "",
             component: () =>
-              import("@/views/student/Code/ProjectDetail/BuildDetail")
+              import("@/views/teacher/Code/ProjectDetail/BuildDetail")
           },
           {
             path: "deploy",
             component: () =>
-              import("@/views/student/Code/ProjectDetail/DeployDetail")
+              import("@/views/teacher/Code/ProjectDetail/DeployDetail")
           },
           {
             path: "test",
             component: () =>
-              import("@/views/student/Code/ProjectDetail/TestDetail")
+              import("@/views/teacher/Code/ProjectDetail/TestDetail")
           }
         ]
       },

+ 69 - 0
src/util/exportExcel.js

@@ -0,0 +1,69 @@
+import XLSX from "xlsx";
+import { saveAs } from "file-saver";
+
+/**
+ * fn 字符串转字符流
+ * @param s
+ * @returns {ArrayBuffer}
+ */
+function s2ab(s) {
+  let buf = new ArrayBuffer(s.length);
+  let view = new Uint8Array(buf);
+  for (let i = 0; i !== s.length; ++i) {
+    view[i] = s.charCodeAt(i) & 0xff;
+  }
+  return buf;
+}
+
+function data2ws(data) {
+  const ws = {};
+  const range = { s: { c: 10000000, r: 10000000 }, e: { c: 0, r: 0 } };
+  for (let R = 0; R !== data.length; ++R) {
+    for (let C = 0; C !== data[R].length; ++C) {
+      if (range.s.r > R) range.s.r = R;
+      if (range.s.c > C) range.s.c = C;
+      if (range.e.r < R) range.e.r = R;
+      if (range.e.c < C) range.e.c = C;
+      const cell = { v: data[R][C] };
+      if (cell.v == null) continue;
+      const cellRef = XLSX.utils.encode_cell({ c: C, r: R });
+      if (typeof cell.v === "number") cell.t = "n";
+      else if (typeof cell.v === "boolean") {
+        cell.t = "b";
+      } else {
+        cell.t = "s";
+      }
+      ws[cellRef] = cell;
+    }
+  }
+  if (range.s.c < 10000000) ws["!ref"] = XLSX.utils.encode_range(range);
+  return ws;
+}
+
+function Workbook() {
+  if (!(this instanceof Workbook)) {
+    return new Workbook();
+  }
+  this.SheetNames = [];
+  this.Sheets = {};
+}
+
+export const toExcel = ({ th, data, fileName, fileType, sheetName }) => {
+  data.unshift(th);
+  const wb = new Workbook();
+  const ws = data2ws(data);
+  sheetName = sheetName || "sheet1";
+  wb.SheetNames.push(sheetName);
+  wb.Sheets[sheetName] = ws;
+  fileType = fileType || "xlsx";
+  var wbout = XLSX.write(wb, {
+    bookType: fileType,
+    bookSST: false,
+    type: "binary"
+  });
+  fileName = fileName || "列表";
+  saveAs(
+    new Blob([s2ab(wbout)], { type: "application/octet-stream" }),
+    `${fileName}.${fileType}`
+  );
+};

+ 54 - 4
src/views/student/Code/ProjectDetail/BuildDetail.vue

@@ -2,8 +2,16 @@
   <div>
     <div v-if="detailLoading"><page-section-loading show /></div>
     <div v-else>
+      <!--空数据处理-->
       <div
-        v-for="item in projectDetail.buildList"
+        class="page-section"
+        v-if="!(projectDetail.buildList && projectDetail.buildList.length > 0)"
+        style="height: 60vh;"
+      >
+        <no-data-svg></no-data-svg>
+      </div>
+      <div
+        v-for="(item, index) in projectDetail.buildList"
         :key="item.id"
         class="gap-section"
       >
@@ -15,12 +23,28 @@
                 {{ item.commitMessage }} · #构建ID:{{ item.id }}
               </strong>
             </div>
+            <div class="level-right">
+              <strong>完成时间:{{ item.buildTime }}</strong>
+            </div>
+          </div>
+          <div class="level">
+            <div class="level-left">
+              <strong style="padding-left: 10px">
+                #branchID:{{ item.branchId }}
+              </strong>
+            </div>
             <div v-if="item.buildState === 1" class="level-right">
               <strong>mirror : {{ item.mirrorId }}</strong>
             </div>
           </div>
-          <div class="code-section">
-            {{ item.buildMessage }} 构建时间: {{ item.time }}ms
+          <a
+            class="button is-info is-small"
+            @click="changeBuildMessageStatus(index)"
+            aria-controls="contentIdForA11y2"
+            >查看详细编译信息</a
+          >
+          <div class="code-section" v-show="buildMessageIsOpen[index]">
+            {{ item.buildMessage }}
           </div>
         </div>
       </div>
@@ -31,9 +55,10 @@
 <script>
 import PageSectionLoading from "@/components/PageBody/PageSectionLoading/index";
 import BuildTag from "@/views/student/Code/ProjectDetail/components/BuildTag";
+import NoDataSvg from "@/components/NoDataSvg";
 export default {
   name: "BuildDetail",
-  components: { BuildTag, PageSectionLoading },
+  components: { BuildTag, PageSectionLoading, NoDataSvg },
   props: {
     projectDetail: {
       type: Object,
@@ -43,6 +68,30 @@ export default {
       type: Boolean,
       default: false
     }
+  },
+  computed: {},
+  watch: {
+    projectDetail: function(newVal) {
+      let buildMessageIsOpen = [];
+      for (let item of newVal.buildList) {
+        //处理buildTime格式
+        item.buildTime = item.buildTime.replace("T", " ") + "ms";
+        buildMessageIsOpen.push(false);
+      }
+      this.buildMessageIsOpen = buildMessageIsOpen;
+    }
+  },
+  data() {
+    return {
+      buildMessageIsOpen: []
+    };
+  },
+  mounted() {},
+  methods: {
+    changeBuildMessageStatus(index) {
+      let status = !this.buildMessageIsOpen[index];
+      this.buildMessageIsOpen.splice(index, 1, status);
+    }
   }
 };
 </script>
@@ -62,5 +111,6 @@ export default {
   padding: $default-margin-between;
   background-color: #2d3044;
   color: #cacff1;
+  white-space: pre-line;
 }
 </style>

+ 265 - 22
src/views/student/Code/ProjectDetail/DeployDetail.vue

@@ -1,67 +1,306 @@
 <template>
   <div class="data-table">
     <section>
+      <div class="mirror-subtitle" v-if="deployStatus != 'RUNNING'">
+        选择镜像
+      </div>
+      <div class="select-mirror" v-if="deployStatus != 'RUNNING'">
+        <div class="latest-build">
+          <div class="test-item" :key="item.id" v-for="item in latestBuildList">
+            <div>
+              <b-radio v-model="radio" :native-value="`b${item.id}`"> </b-radio>
+              <strong>构建ID: #</strong> {{ item.id }}
+              <b-tag type="is-success">success</b-tag>
+              <span style="padding-left: 10px">
+                <strong>镜像ID: #</strong> {{ item.mirrorId }}
+              </span>
+            </div>
+          </div>
+          <div
+            class="test-item"
+            :key="item.id"
+            v-for="item in latestDeployList"
+          >
+            <div>
+              <b-radio v-model="radio" :native-value="`d${item.id}`"> </b-radio>
+              <strong>部署ID: #</strong> {{ item.id }}
+              <b-tag type="is-success">success</b-tag>
+              <span style="padding-left: 10px">
+                <strong>镜像ID: #</strong> {{ item.mirrorId }}
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
       <button
-        :class="['button', 'block', 'is-link', { 'is-loading': isActive }]"
-        @click="isActive = !isActive"
+        :class="['button', 'block', 'is-link', { 'is-loading': isRunning }]"
+        @click="startDeploy"
+        style="margin-top: 20px;"
       >
         开始部署
       </button>
       <b-message
         title="部署进行中..."
-        :active.sync="isActive"
+        :active.sync="isRunning"
         type="is-warning"
       >
         部署已经开始,一次部署需要大约5-10分钟,请5-10分钟后刷新页面获取部署结果
       </b-message>
     </section>
     <!--部署记录-->
-    <div class="deploy-list">
-      <div class="subtitle">部署结果</div>
+    <div class="deploy-list" v-if="deployInfo">
+      <div class="subtitle">
+        {{ isRunning ? "当前部署情况" : "上次部署结果" }}
+      </div>
       <div class="deploy-item">本次部署ID: #{{ deployInfo.id }}</div>
-      <div class="deploy-item">本次部署时间: 2019-01-14 15:05</div>
+      <div class="deploy-item">本次部署镜像ID: #{{ deployInfo.mirrorId }}</div>
       <div class="deploy-item">
-        部署结果:
-        <div class="tag is-success">部署成功</div>
+        本次部署创建时间: {{ handleTime(deployInfo.createdAt) }}
       </div>
-      <div class="deploy-item">
-        <div class="subtitle">项目部署地址</div>
-        <a :href="`http://${deployInfo.pageUrl}`" target="_blank">查看详情</a>
+      <div class="deploy-item" v-if="deployInfo.endAt">
+        本次部署结束时间: {{ handleTime(deployInfo.endAt) }}
       </div>
       <div class="deploy-item">
-        <div class="subtitle">配置信息</div>
-        <div>暂无项目配置信息</div>
+        部署结果:
+        <div :class="['tag', type(deployInfo.status)]">
+          {{ deployInfo.status }}
+        </div>
       </div>
+      <div class="subtitle">项目部署地址</div>
+      <div class="deploy-item">{{ deployInfo.url }}</div>
       <div class="deploy-item">
         <div class="subtitle">部署输出信息</div>
-        <div class="code-section">
-          <div class="code-content">{{ deployInfo.deployMessage }}</div>
+        <a
+          class="button is-small is-info"
+          @click="unfoldDeployLogInfo = !unfoldDeployLogInfo"
+        >
+          {{ unfoldDeployLogInfo ? "收起部署详细信息" : "查看部署详细信息" }}</a
+        >
+        <div class="code-section" v-show="unfoldDeployLogInfo">
+          <div class="code-content" v-html="deployLog"></div>
         </div>
-        <!--<div class="code-section">{{deployInfo.deployMessage}}</div>-->
       </div>
     </div>
   </div>
 </template>
 <script>
-import { getDeployDetail } from "@/api/codehw";
+import {
+  getDeployDetail,
+  getDeployLogInfo,
+  getLatestBuildInfoList,
+  makeDeploy,
+  getLatestDeployInfoList
+} from "@/api/codehw";
 
 export default {
   data() {
     return {
       isActive: false,
-      deployInfo: {}
+      isRunning: false,
+      deployStatus: "",
+      deployInfo: {},
+      haveDeployRecord: false,
+      deployLog: "",
+      unfoldDeployLogInfo: false,
+      latestBuildList: [],
+      latestDeployList: [],
+      latestMirrorIdList: [],
+      radio: -1
     };
   },
+  props: {
+    projectDetail: {
+      type: Object,
+      default: () => {}
+    },
+    detailLoading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    projectDetail: function(newVal) {
+      console.log(newVal);
+      //获取最新一次部署详细信息
+      if (newVal.latestDeploy && newVal.latestDeploy.id) {
+        this.haveDeployRecord = true;
+        this.getLatestDeployInfo(newVal.latestDeploy.id);
+        // this.getLatestDeployInfo(17);
+        this.getDeployLogInfo(this.projectDetail.latestDeploy.id);
+      }
+      this.getLatestBuildInfo(this.projectDetail && this.projectDetail.id, 5);
+      this.getLastNDeployInfoList(
+        this.projectDetail && this.projectDetail.id,
+        5
+      );
+    }
+  },
   mounted() {
-    getDeployDetail(123).then(res => {
-      console.log(res.data);
-      this.deployInfo = res.data;
-    });
+    if (this.projectDetail.latestDeploy) {
+      this.getLatestDeployInfo(this.projectDetail.latestDeploy.id);
+      this.getDeployLogInfo(this.projectDetail.latestDeploy.id);
+    }
+    if (this.projectDetail.id) {
+      this.getLatestBuildInfo(this.projectDetail.id, 5);
+      this.getLastNDeployInfoList(this.projectDetail.id, 5);
+    }
+  },
+  methods: {
+    //获取最新一次部署详细信息
+    getLatestDeployInfo(deployId) {
+      getDeployDetail(deployId)
+        .then(res => {
+          this.deployInfo = res.data;
+          this.deployStatus = res.data.status;
+          let isRunning = this.deployStatus == "RUNNING" ? true : false;
+          this.isRunning = isRunning;
+        })
+        .catch(e => {
+          this.$toast.open({
+            message: e.message || "服务器未知错误",
+            type: "is-danger"
+          });
+        });
+    },
+    //处理时间格式
+    handleTime(time) {
+      if (!time) {
+        return "";
+      }
+      return time.replace("T", " ") + "ms";
+    },
+    //开始部署
+    startDeploy() {
+      if (this.radio == -1) {
+        this.$toast.open({
+          message: "请先选择镜像",
+          type: "is-danger"
+        });
+        return;
+      } else {
+        let mirrorId = this.getSelectedMirrorId();
+        //触发部署
+        makeDeploy({
+          projectId: this.projectDetail.id,
+          mirrorId: mirrorId,
+          replicas: 1
+        })
+          .then(res => {
+            console.log(res);
+            this.$toast.open({
+              message: "开始部署",
+              type: "is-success"
+            });
+            this.isRunning = true;
+            setTimeout(() => {
+              this.getLatestDeployInfo(this.projectDetail.latestDeploy.id);
+            }, 1000);
+          })
+          .then(e => {
+            this.$toast.open({
+              message: e.message || "服务器未知错误",
+              type: "is-danger"
+            });
+          });
+      }
+    },
+    //获取最近n次部署成功记录
+    getLastNDeployInfoList(projectId, limit) {
+      getLatestDeployInfoList(projectId, limit)
+        .then(res => {
+          this.latestDeployList = res.data;
+          // this.latestMirrorIdList = this.latestMirrorIdList.concat(res.data);
+        })
+        .catch(e => {
+          this.$toast.open({
+            message: e.message || " 服务器未知错误",
+            type: "is-danger"
+          });
+        });
+    },
+    //获取最近n个构建的信息
+    getLatestBuildInfo(projectId, limit) {
+      getLatestBuildInfoList(projectId, limit)
+        .then(res => {
+          this.latestBuildList = res.data;
+          // this.latestMirrorIdList = this.latestMirrorIdList.concat(res.data);
+        })
+        .catch(e => {
+          this.$toast.open({
+            message: e.message || "服务器未知错误",
+            type: "is-danger"
+          });
+        });
+    },
+    //获取部署日志
+    getDeployLogInfo(deployId) {
+      getDeployLogInfo(deployId)
+        .then(res => {
+          this.deployLog = res.data;
+        })
+        .catch(e => {
+          this.$toast.open({
+            message: e.message || "服务器未知错误",
+            type: "is-danger"
+          });
+        });
+    },
+    //部署类型
+    type(value) {
+      if (value == "FAILURE") {
+        return "is-danger";
+      } else if (value == "RUNNING") {
+        return "is-warning";
+      } else if (value == "SUCCESS") {
+        return "is-success";
+      } else {
+        return "";
+      }
+    },
+    //选择镜像ID
+    getSelectedMirrorId() {
+      let list =
+        this.radio[0] == "b" ? this.latestBuildList : this.latestDeployList;
+      let id = this.radio.substring(1);
+      for (let item of list) {
+        if (item.id == id) {
+          return item.mirrorId;
+        }
+      }
+    }
   }
 };
 </script>
 <style lang="scss" scoped>
 @import "../../../../assets/scss/app";
+.mirror-subtitle {
+  font-size: 1.25rem;
+  width: 100%;
+  font-weight: bold;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #dbdbdb;
+}
+.select-mirror {
+  width: 100%;
+  display: flex;
+  .latest-build {
+    width: 100%;
+  }
+}
+.test-item {
+  width: 100%;
+  padding: $default-margin $default-margin-between;
+  margin: 0 -#{$default-margin-between};
+  cursor: pointer;
+}
+/*.test-item:hover,*/
+/*.test-item:nth-child(even):hover {*/
+/*background-color: #dde3ef;*/
+/*}*/
+
+.test-item:nth-child(even) {
+  background-color: rgba(244, 247, 252, 0.6);
+}
 .deploy-list {
   margin-top: 1.5rem;
   .deploy-item {
@@ -71,6 +310,9 @@ export default {
       margin-left: 10px;
       margin-right: 10px;
     }
+    a {
+      margin-bottom: 10px;
+    }
   }
   .subtitle {
     margin-top: 1.5rem;
@@ -83,6 +325,7 @@ export default {
 .code-section {
   background-color: #2d3044;
   padding: 20px 20px 20px 20px;
+  white-space: pre-line;
   .code-content {
     color: #cacff1;
   }

+ 15 - 6
src/views/student/Code/ProjectDetail/Test/FunctionalTestDetail.vue

@@ -24,10 +24,19 @@
         <div class="tag">{{ displayUnixTime(testDetail.time) }}</div>
       </template>
     </page-header>
-    <div class="console-section">{{ testDetail.consoleOutput }}</div>
     <page-body>
       <div v-if="testDetailLoading"><page-section-loading show /></div>
       <div v-else>
+        <div class="gap-section">
+          <div class="page-section">
+            <a class="button is-small is-info" @click="showLog = !showLog">{{
+              showLog ? "收起控制台信息" : "查看控制台输出"
+            }}</a>
+            <div class="console-section" v-if="showLog">
+              {{ testDetail.consoleOutput }}
+            </div>
+          </div>
+        </div>
         <div
           v-for="item in testDetail.detail"
           :key="item.id"
@@ -35,15 +44,13 @@
         >
           <div class="page-section">
             <div>
-              <pass-tag :pass="item.isPass" />
+              <pass-tag :pass="item.pass" />
               <strong style="padding-left: 10px">
                 页面测试用例 : #{{ item.id }}
               </strong>
             </div>
-            <div v-if="!item.isPass" class="code-section">
+            <div v-if="!item.pass" class="code-section">
               {{ item.message }} <br />
-              <br />
-              {{ item.trace }}
             </div>
           </div>
         </div>
@@ -64,7 +71,8 @@ export default {
   data() {
     return {
       testDetail: { detail: [] },
-      testDetailLoading: false
+      testDetailLoading: false,
+      showLog: false
     };
   },
   mixins: [timeMixins],
@@ -100,5 +108,6 @@ export default {
   padding: $default-margin-between $default-margin-between * 1.5;
   background-color: #2d3044;
   color: #cacff1;
+  white-space: pre-line;
 }
 </style>

+ 4 - 2
src/views/student/Code/ProjectDetail/Test/UnitTestDetail.vue

@@ -35,12 +35,12 @@
         >
           <div class="page-section">
             <div>
-              <pass-tag :pass="item.isPass" />
+              <pass-tag :pass="item.pass" />
               <strong style="padding-left: 10px">
                 页面测试用例 : #{{ item.id }}
               </strong>
             </div>
-            <div v-if="!item.isPass" class="code-section">
+            <div v-if="!item.pass" class="code-section">
               {{ item.message }} <br />
               <br />
               {{ item.trace }}
@@ -94,11 +94,13 @@ export default {
   padding: $default-margin-between;
   background-color: #2d3044;
   color: #cacff1;
+  white-space: pre-line;
 }
 
 .console-section {
   padding: $default-margin-between $default-margin-between * 1.5;
   background-color: #2d3044;
   color: #cacff1;
+  white-space: pre-line;
 }
 </style>

+ 30 - 22
src/views/student/Code/ProjectDetail/TestDetail.vue

@@ -4,6 +4,9 @@
       <div v-if="detailLoading"><page-section-loading show /></div>
       <div v-else class="page-section">
         <h5 class="title is-5">单元测试:</h5>
+        <div v-if="!(unitTestList && unitTestList.length > 0)">
+          <strong>当前暂无单元测试数据</strong>
+        </div>
         <router-link
           class="test-item"
           :key="item.id"
@@ -25,14 +28,22 @@
         <h5 class="test-title title is-5">
           页面测试:
           <div
-            v-if="functionTestList && functionTestList.length > 0"
             :class="{ 'is-loading': testing }"
             @click="makeTest"
+            :disabled="
+              !(
+                projectDetail.latestDeploy &&
+                projectDetail.latestDeploy.status == 'SUCCESS'
+              )
+            "
             class="button is-primary test-title__action"
           >
             触发测试
           </div>
         </h5>
+        <div v-if="!(functionTestList && functionTestList.length > 0)">
+          <strong>当前暂无页面测试数据</strong>
+        </div>
         <router-link
           class="test-item"
           :key="item.id"
@@ -87,28 +98,25 @@ export default {
   mixins: [timeMixins],
   methods: {
     async makeTest() {
-      const { functionTestList } = this;
-      if (functionTestList && functionTestList.length > 0) {
-        this.testing = true;
-        const { deployId } = functionTestList[0];
-        const { homeworkId, projectId } = this.$route.params;
-        const { data } = await makeFunctionalTest({
-          homeworkId,
-          projectId,
-          deployId
+      this.testing = true;
+      const deployId = this.projectDetail.latestDeploy.id;
+      const { homeworkId, projectId } = this.$route.params;
+      const { data } = await makeFunctionalTest({
+        homeworkId,
+        projectId,
+        deployId
+      });
+      this.testing = false;
+      if (data.success) {
+        this.$toast.open({
+          message: `测试触发成功`,
+          type: "is-success"
+        });
+      } else {
+        this.$toast.open({
+          message: `测试触发失败`,
+          type: "is-danger"
         });
-        this.testing = false;
-        if (data.success) {
-          this.$toast.open({
-            message: `测试触发成功`,
-            type: "is-success"
-          });
-        } else {
-          this.$toast.open({
-            message: `测试触发失败`,
-            type: "is-danger"
-          });
-        }
       }
     },
     sortTestList(list) {

+ 2 - 1
src/views/student/Code/ProjectDetail/index.vue

@@ -31,7 +31,7 @@
       </template>
       <template slot="content">
         <strong>git 地址 : </strong>
-        <a :href="projectDetail.url">{{ projectDetail.url }}</a>
+        <a :href="projectDetail.url" target="_blank">{{ projectDetail.url }}</a>
       </template>
     </page-header>
     <page-body>
@@ -62,6 +62,7 @@ export default {
     const { projectId } = this.$route.params;
     this.detailLoading = true;
     const detail = await getProjectDetail(projectId);
+    console.log(detail.data);
     this.projectDetail = detail.data;
     this.detailLoading = false;
   }

+ 17 - 2
src/views/student/Code/index.vue

@@ -23,9 +23,20 @@
           class="column is-4-desktop"
         >
           <router-link :to="`${$route.path}/${item.id}`">
-            <card hoverable>{{ item.name }}</card>
+            <card hoverable>
+              {{ item.name }}
+              <div><code-state-tag :state="item.status"></code-state-tag></div>
+            </card>
           </router-link>
         </div>
+        <!--当没有代码作业时的处理-->
+        <no-data-svg
+          style="margin-top: 20px;"
+          v-if="
+            !codeHomeworkListLoading &&
+              !(codeHomeworkList && codeHomeworkList.length > 0)
+          "
+        ></no-data-svg>
       </div>
     </page-body>
   </div>
@@ -37,11 +48,15 @@ import Card from "@/components/Card";
 import PageHeader from "@/components/PageHeader";
 import PageBody from "@/components/PageBody/index";
 import { getCodeAssignmentListByStu } from "@/api/assignment";
+import CodeStateTag from "@/components/DocumentStateTag";
+import noDataSvg from "@/components/NoDataSvg";
 export default {
   components: {
     PageBody,
     PageHeader,
-    Card
+    Card,
+    noDataSvg,
+    CodeStateTag
   },
   computed: {
     ...mapState({

+ 7 - 1
src/views/student/Document/index.vue

@@ -31,6 +31,10 @@
             </card>
           </router-link>
         </div>
+        <no-data-svg
+          style="margin-top: 20px;"
+          v-if="!loadingDocList && !(docList && docList.length > 0)"
+        ></no-data-svg>
       </div>
     </page-body>
   </div>
@@ -43,12 +47,14 @@ import { getDocAssignmentListByCrs } from "@/api/assignment";
 import timeMixins from "@/mixins/time";
 import Card from "@/components/Card";
 import DocumentStateTag from "@/components/DocumentStateTag";
+import NoDataSvg from "@/components/NoDataSvg";
 export default {
   components: {
     PageBody,
     PageHeader,
     Card,
-    DocumentStateTag
+    DocumentStateTag,
+    NoDataSvg
   },
   mixins: [timeMixins],
   mounted() {

+ 1 - 1
src/views/student/Home/index.vue

@@ -24,7 +24,7 @@
           v-for="item in courses"
           class="column is-4-desktop"
         >
-          <router-link :to="`/course-student/${item.id}/dashboard`">
+          <router-link :to="`/course-student/${item.id}/group`">
             <card hoverable>{{ item.name }}</card>
           </router-link>
         </div>

+ 7 - 2
src/views/student/Review/ReviewDetail/ReviewSelfDetail/index.vue

@@ -13,6 +13,10 @@
         <div class="right-part">
           <page-section-loading :show="selfDetailLoading" />
           <div v-if="!selfDetailLoading">
+            <div v-if="!!detail.level" class="notification is-success">
+              <strong>您的得分 : </strong>{{ detail.level }}
+            </div>
+
             <result-check-list-item
               v-for="item in detail.checkListResult"
               :arguable="detail.review.state === 'ARGUING'"
@@ -64,8 +68,9 @@ export default {
     async argue(item) {
       const { reviewId } = this.$route.params;
       try {
-        const { data } = await postStudentArgueReview({ reviewId, ...item });
-        this.detail.checkList = data;
+        await postStudentArgueReview({ reviewId, ...item });
+        const { data } = await getStudentReviewSelfDetail(reviewId);
+        this.detail = data;
       } catch (e) {
         this.$toast.open({
           message: `重复的申请评审是不被允许的`,

+ 11 - 7
src/views/student/Review/ReviewDocDetail/index.vue

@@ -31,14 +31,17 @@
           <div class="right-part">
             <div v-if="fetchLoading"><page-section-loading show /></div>
             <div v-else>
-              <div
-                class="button  is-medium is-primary action-button"
-                :class="{ 'is-loading': submitLoading }"
-                @click="submitCheckList"
-              >
-                提 交
+              <div v-if="detail.review.state === 'REVIEWING'">
+                <div
+                  class="button  is-medium is-primary action-button"
+                  :class="{ 'is-loading': submitLoading }"
+                  @click="submitCheckList"
+                >
+                  提 交
+                </div>
               </div>
               <check-list
+                :review="detail.review"
                 v-model="detail.checkList"
                 @submit-solution="submitSolution"
                 @accept-argue="acceptArgue"
@@ -81,7 +84,8 @@ export default {
         checkList: []
       },
       submitLoading: false,
-      loadingDocContent: false
+      loadingDocContent: false,
+      fetchLoading: false
     };
   },
   mixins: [timeMixins],

+ 7 - 1
src/views/student/Review/index.vue

@@ -29,6 +29,10 @@
             </card>
           </router-link>
         </div>
+        <no-data-svg
+          style="margin-top: 20px;"
+          v-if="!loadingReviewList && !(review && review.length > 0)"
+        ></no-data-svg>
       </div>
     </page-body>
   </div>
@@ -40,12 +44,14 @@ import Card from "@/components/Card";
 import PageHeader from "@/components/PageHeader";
 import PageBody from "@/components/PageBody/index";
 import ReviewStateTag from "@/components/ReviewStateTag/index";
+import NoDataSvg from "@/components/NoDataSvg";
 export default {
   components: {
     ReviewStateTag,
     PageBody,
     PageHeader,
-    Card
+    Card,
+    NoDataSvg
   },
   data() {
     return {

+ 118 - 0
src/views/teacher/Code/ProjectDetail/BuildDetail.vue

@@ -0,0 +1,118 @@
+<template>
+  <div>
+    <div v-if="detailLoading"><page-section-loading show /></div>
+    <div v-else>
+      <!--空数据处理-->
+      <div
+        class="page-section"
+        v-if="!(projectDetail.buildList && projectDetail.buildList.length > 0)"
+        style="height: 60vh;"
+      >
+        <no-data-svg></no-data-svg>
+      </div>
+      <div
+        v-for="(item, index) in projectDetail.buildList"
+        :key="item.id"
+        class="gap-section"
+      >
+        <div class="page-section">
+          <div class="level">
+            <div class="level-left">
+              <build-tag :build-state="item.buildState" />
+              <strong style="padding-left: 10px">
+                {{ item.commitMessage }} · #构建ID:{{ item.id }}
+              </strong>
+            </div>
+            <div class="level-right">
+              <strong>完成时间:{{ item.buildTime }}</strong>
+            </div>
+          </div>
+          <div class="level">
+            <div class="level-left">
+              <strong style="padding-left: 10px">
+                #branchID:{{ item.branchId }}
+              </strong>
+            </div>
+            <div v-if="item.buildState === 1" class="level-right">
+              <strong>mirror : {{ item.mirrorId }}</strong>
+            </div>
+          </div>
+          <a
+            class="button is-info is-small"
+            @click="changeBuildMessageStatus(index)"
+            aria-controls="contentIdForA11y2"
+            >查看详细编译信息</a
+          >
+          <div class="code-section" v-show="buildMessageIsOpen[index]">
+            <p v-html="item.buildMessage"></p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import PageSectionLoading from "@/components/PageBody/PageSectionLoading/index";
+import BuildTag from "@/views/student/Code/ProjectDetail/components/BuildTag";
+import NoDataSvg from "@/components/NoDataSvg";
+export default {
+  name: "BuildDetail",
+  components: { BuildTag, PageSectionLoading, NoDataSvg },
+  props: {
+    projectDetail: {
+      type: Object,
+      default: () => {}
+    },
+    detailLoading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {},
+  watch: {
+    projectDetail: function(newVal) {
+      let buildMessageIsOpen = [];
+      for (let i = 0; i < newVal.buildList.length; i++) {
+        let buildInfo = newVal.buildList[i];
+        buildInfo.buildTime = buildInfo.buildTime.replace("T", " ") + "ms";
+        buildMessageIsOpen.push(false);
+        this.projectDetail.buildList[i] = buildInfo;
+      }
+      this.buildMessageIsOpen = buildMessageIsOpen;
+    }
+  },
+  data() {
+    return {
+      buildMessageIsOpen: [],
+      testBuildMessage: "测试换行<br>第一行<br>第二行<br>"
+    };
+  },
+  mounted() {},
+  methods: {
+    changeBuildMessageStatus(index) {
+      let status = !this.buildMessageIsOpen[index];
+      this.buildMessageIsOpen.splice(index, 1, status);
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../../assets/scss/app";
+.gap-section {
+  padding-bottom: $default-margin-between;
+}
+
+.gap-section:last-child {
+  padding-bottom: 0;
+}
+
+.code-section {
+  margin: $default-margin-between -#{$default-margin-between} -#{$default-margin-between};
+  padding: $default-margin-between;
+  background-color: #2d3044;
+  color: #cacff1;
+  white-space: pre-line;
+}
+</style>

+ 172 - 0
src/views/teacher/Code/ProjectDetail/DeployDetail.vue

@@ -0,0 +1,172 @@
+<template>
+  <div class="data-table">
+    <section>
+      <b-message
+        title="部署进行中..."
+        :active.sync="isRunning"
+        type="is-warning"
+      >
+        部署已经开始,一次部署需要大约5-10分钟,请5-10分钟后刷新页面获取部署结果
+      </b-message>
+    </section>
+    <!--部署记录-->
+    <div class="deploy-list" v-if="deployInfo">
+      <div class="subtitle">
+        {{ isRunning ? "当前部署情况" : "上次部署结果" }}
+      </div>
+      <div class="deploy-item">本次部署ID: #{{ deployInfo.id }}</div>
+      <div class="deploy-item">
+        本次部署创建时间: {{ handleTime(deployInfo.createdAt) }}
+      </div>
+      <div class="deploy-item" v-if="deployInfo.endAt">
+        本次部署结束时间: {{ handleTime(deployInfo.endAt) }}
+      </div>
+      <div class="deploy-item">
+        部署结果:
+        <div :class="['tag', type(deployInfo.status)]">
+          {{ deployInfo.status }}
+        </div>
+      </div>
+      <div class="subtitle">项目部署地址</div>
+      <div class="deploy-item">{{ deployInfo.url }}</div>
+      <div class="deploy-item">
+        <div class="subtitle">部署输出信息</div>
+        <a
+          class="button is-small is-info"
+          @click="unfoldDeployLogInfo = !unfoldDeployLogInfo"
+        >
+          {{ unfoldDeployLogInfo ? "收起部署详细信息" : "查看部署详细信息" }}</a
+        >
+        <div class="code-section" v-show="unfoldDeployLogInfo">
+          <div class="code-content" v-html="deployLog"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import { getDeployDetail, getDeployLogInfo } from "@/api/codehw";
+
+export default {
+  data() {
+    return {
+      isActive: false,
+      isRunning: false,
+      deployStatus: "",
+      deployInfo: {},
+      haveDeployRecord: false,
+      deployLog: "",
+      unfoldDeployLogInfo: false
+    };
+  },
+  props: {
+    projectDetail: {
+      type: Object,
+      default: () => {}
+    },
+    detailLoading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    projectDetail: function(newVal) {
+      console.log(newVal);
+      //获取最新一次部署详细信息
+      if (newVal.latestDeploy && newVal.latestDeploy.id) {
+        this.haveDeployRecord = true;
+        this.getLatestDeployInfo(newVal.latestDeploy.id);
+        this.getDeployLogInfo(this.projectDetail.latestDeploy.id);
+      }
+    }
+  },
+  mounted() {
+    if (this.projectDetail.latestDeploy) {
+      this.getLatestDeployInfo(this.projectDetail.latestDeploy.id);
+      this.getDeployLogInfo(this.projectDetail.latestDeploy.id);
+    }
+  },
+  methods: {
+    //获取最新一次部署详细信息
+    getLatestDeployInfo(deployId) {
+      getDeployDetail(deployId)
+        .then(res => {
+          this.deployInfo = res.data;
+          this.deployStatus = res.data.status;
+          let isRunning = this.deployStatus == "RUNNING" ? true : false;
+          this.isRunning = isRunning;
+        })
+        .catch(e => {
+          this.$toast.open({
+            message: e.message || "服务器未知错误",
+            type: "is-danger"
+          });
+        });
+    },
+    //处理时间格式
+    handleTime(time) {
+      if (!time) {
+        return "";
+      }
+      return time.replace("T", " ") + "ms";
+    },
+    //获取部署日志
+    getDeployLogInfo(deployId) {
+      getDeployLogInfo(deployId)
+        .then(res => {
+          this.deployLog = res.data;
+        })
+        .catch(e => {
+          this.$toast.open({
+            message: e.message || "服务器未知错误",
+            type: "is-danger"
+          });
+        });
+    },
+    //部署类型
+    type(value) {
+      if (value == "FAILURE") {
+        return "is-danger";
+      } else if (value == "RUNNING") {
+        return "is-warning";
+      } else if (value == "SUCCESS") {
+        return "is-success";
+      } else {
+        return "";
+      }
+    }
+  }
+};
+</script>
+<style lang="scss" scoped>
+@import "../../../../assets/scss/app";
+.deploy-list {
+  margin-top: 1.5rem;
+  .deploy-item {
+    margin-top: 10px;
+    font-weight: bold;
+    .tag {
+      margin-left: 10px;
+      margin-right: 10px;
+    }
+    a {
+      margin-bottom: 10px;
+    }
+  }
+  .subtitle {
+    margin-top: 1.5rem;
+    width: 100%;
+    font-weight: bold;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #dbdbdb;
+  }
+}
+.code-section {
+  background-color: #2d3044;
+  padding: 20px 20px 20px 20px;
+  white-space: pre-line;
+  .code-content {
+    color: #cacff1;
+  }
+}
+</style>

+ 156 - 0
src/views/teacher/Code/ProjectDetail/TestDetail.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="columns is-desktop">
+    <div class="column">
+      <div v-if="detailLoading"><page-section-loading show /></div>
+      <div v-else class="page-section">
+        <h5 class="title is-5">单元测试:</h5>
+        <div v-if="!(unitTestList && unitTestList.length > 0)">
+          <strong>当前暂无单元测试数据</strong>
+        </div>
+        <router-link
+          class="test-item"
+          :key="item.id"
+          :to="`${$route.path}/unit/${item.id}`"
+          v-for="item in unitTestList"
+          tag="div"
+        >
+          <div><strong>UNIT TEST : #</strong> {{ item.id }}</div>
+          <pass-tag :pass="item.isPassAll" />
+          <span style="padding-left: 10px">{{
+            displayUnixTime(item.time)
+          }}</span>
+        </router-link>
+      </div>
+    </div>
+    <div class="column">
+      <div v-if="detailLoading"><page-section-loading show /></div>
+      <div v-else class="page-section">
+        <h5 class="test-title title is-5">
+          页面测试:
+          <!--<div-->
+          <!--:class="{ 'is-loading': testing }"-->
+          <!--@click="makeTest"-->
+          <!--:disabled="!(functionTestList && functionTestList.length > 0)"-->
+          <!--class="button is-primary test-title__action"-->
+          <!--&gt;-->
+          <!--触发测试-->
+          <!--</div>-->
+        </h5>
+        <div v-if="!(functionTestList && functionTestList.length > 0)">
+          <strong>当前暂无页面测试数据</strong>
+        </div>
+        <router-link
+          class="test-item"
+          :key="item.id"
+          :to="`${$route.path}/functional/${item.id}`"
+          v-for="item in functionTestList"
+          tag="div"
+        >
+          <div><strong>FUNCTIONAL TEST : #</strong> {{ item.id }}</div>
+          <pass-tag :pass="item.isPassAll" />
+          <span style="padding-left: 10px">{{
+            displayUnixTime(item.time)
+          }}</span>
+        </router-link>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import PassTag from "@/views/student/Code/ProjectDetail/components/PassTag";
+import timeMixins from "@/mixins/time";
+import { makeFunctionalTest } from "@/api/codehw";
+import PageSectionLoading from "@/components/PageBody/PageSectionLoading/index";
+export default {
+  components: { PageSectionLoading, PassTag },
+  props: {
+    projectDetail: {
+      type: Object,
+      default: () => ({
+        unitTestList: [],
+        functionTestList: []
+      })
+    },
+    detailLoading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    unitTestList() {
+      return this.sortTestList(this.projectDetail.unitTestList);
+    },
+    functionTestList() {
+      return this.sortTestList(this.projectDetail.functionTestList);
+    }
+  },
+  data() {
+    return {
+      testing: false
+    };
+  },
+  mixins: [timeMixins],
+  methods: {
+    async makeTest() {
+      const { functionTestList } = this;
+      if (functionTestList && functionTestList.length > 0) {
+        this.testing = true;
+        const { deployId } = functionTestList[0];
+        const { homeworkId, projectId } = this.$route.params;
+        const { data } = await makeFunctionalTest({
+          homeworkId,
+          projectId,
+          deployId
+        });
+        this.testing = false;
+        if (data.success) {
+          this.$toast.open({
+            message: `测试触发成功`,
+            type: "is-success"
+          });
+        } else {
+          this.$toast.open({
+            message: `测试触发失败`,
+            type: "is-danger"
+          });
+        }
+      }
+    },
+    sortTestList(list) {
+      console.log(list);
+      if (!list) return [];
+      const copyList = list.slice(0);
+      copyList.sort((a, b) => b.time - a.time);
+      return copyList;
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../../../assets/scss/app";
+.test-item {
+  padding: $default-margin $default-margin-between;
+  margin: 0 -#{$default-margin-between};
+  cursor: pointer;
+}
+
+.test-title {
+  position: relative;
+  &__action {
+    position: absolute;
+    right: 0;
+    transform: translateY(-20%);
+  }
+}
+
+.test-item:hover,
+.test-item:nth-child(even):hover {
+  background-color: #dde3ef;
+}
+
+.test-item:nth-child(even) {
+  background-color: rgba(244, 247, 252, 0.6);
+}
+</style>

+ 8 - 2
src/views/teacher/Code/index.vue

@@ -20,8 +20,8 @@
     </page-header>
 
     <page-body>
+      <page-section-loading :show="loadingCodeList" />
       <div class="data-table">
-        <page-section-loading :show="loadingCodeList" />
         <b-table
           v-if="!loadingCodeList"
           :data="codeData"
@@ -82,6 +82,10 @@
             </article>
           </template>
         </b-table>
+        <no-data-svg
+          style="margin-top: 20px;"
+          v-if="!loadingCodeList && !(codeData && codeData.length > 0)"
+        ></no-data-svg>
       </div>
     </page-body>
   </div>
@@ -97,6 +101,7 @@ import DeleteAssignment from "@/components/AssignmentCRUD/DeleteAssignment";
 import UpdateAssignment from "@/components/AssignmentCRUD/UpdateAssignment";
 import CreateAssignment from "@/components/AssignmentCRUD/CreateAssignment";
 import PageSectionLoading from "@/components/PageBody/PageSectionLoading/index";
+import NoDataSvg from "@/components/NoDataSvg";
 
 export default {
   components: {
@@ -105,7 +110,8 @@ export default {
     DeleteAssignment,
     UpdateAssignment,
     CreateAssignment,
-    PageSectionLoading
+    PageSectionLoading,
+    NoDataSvg
   },
   mixins: [timeMixins],
   mounted() {

+ 7 - 1
src/views/teacher/Document/index.vue

@@ -78,6 +78,10 @@
               </article>
             </template>
           </b-table>
+          <no-data-svg
+            style="margin-top: 20px;"
+            v-if="!loadingDocList && !(docData && docData.length > 0)"
+          ></no-data-svg>
         </div>
       </div>
     </page-body>
@@ -94,6 +98,7 @@ import DeleteAssignment from "@/components/AssignmentCRUD/DeleteAssignment";
 import UpdateAssignment from "@/components/AssignmentCRUD/UpdateAssignment";
 import CreateAssignment from "@/components/AssignmentCRUD/CreateAssignment";
 import PageSectionLoading from "@/components/PageBody/PageSectionLoading/index";
+import NoDataSvg from "@/components/NoDataSvg";
 
 export default {
   computed: {
@@ -107,7 +112,8 @@ export default {
     DeleteAssignment,
     UpdateAssignment,
     CreateAssignment,
-    PageSectionLoading
+    PageSectionLoading,
+    NoDataSvg
   },
   mixins: [timeMixins],
   mounted() {

+ 1 - 1
src/views/teacher/Home/index.vue

@@ -19,7 +19,7 @@
           <card loading /> <card loading /> <card loading /> <card loading />
         </div>
         <div v-else :key="item.id" v-for="item in courses" class="column is-4">
-          <router-link :to="`/course-teacher/${item.id}/dashboard`">
+          <router-link :to="`/course-teacher/${item.id}/question`">
             <card hoverable>{{ item.name }}</card>
           </router-link>
         </div>

+ 5 - 256
src/views/teacher/Question/index.vue

@@ -173,261 +173,8 @@
               </ul>
             </div>
           </div>
-          <!--当没有数据时页面展示-->
-          <div class="no-data" v-if="data.length <= 0">
-            <svg
-              id="f20e0c25-d928-42cc-98d1-13cc230663ea"
-              data-name="Layer 1"
-              xmlns="http://www.w3.org/2000/svg"
-              xmlns:xlink="http://www.w3.org/1999/xlink"
-              width="620.16"
-              height="500.81"
-              viewBox="0 0 820.16 780.81"
-            >
-              <defs>
-                <linearGradient
-                  id="07332201-7176-49c2-9908-6dc4a39c4716"
-                  x1="539.63"
-                  y1="734.6"
-                  x2="539.63"
-                  y2="151.19"
-                  gradientTransform="translate(-3.62 1.57)"
-                  gradientUnits="userSpaceOnUse"
-                >
-                  <stop offset="0" stop-color="gray" stop-opacity="0.25" />
-                  <stop offset="0.54" stop-color="gray" stop-opacity="0.12" />
-                  <stop offset="1" stop-color="gray" stop-opacity="0.1" />
-                </linearGradient>
-                <linearGradient
-                  id="0ee1ab3f-7ba2-4205-9d4a-9606ad702253"
-                  x1="540.17"
-                  y1="180.2"
-                  x2="540.17"
-                  y2="130.75"
-                  gradientTransform="translate(-63.92 7.85)"
-                  xlink:href="#07332201-7176-49c2-9908-6dc4a39c4716"
-                />
-                <linearGradient
-                  id="abca9755-bed1-4a97-b027-7f02ee3ffa09"
-                  x1="540.17"
-                  y1="140.86"
-                  x2="540.17"
-                  y2="82.43"
-                  gradientTransform="translate(-84.51 124.6) rotate(-12.11)"
-                  xlink:href="#07332201-7176-49c2-9908-6dc4a39c4716"
-                />
-                <linearGradient
-                  id="2632d424-e666-4ee4-9508-a494957e14ab"
-                  x1="476.4"
-                  y1="710.53"
-                  x2="476.4"
-                  y2="127.12"
-                  gradientTransform="matrix(1, 0, 0, 1, 0, 0)"
-                  xlink:href="#07332201-7176-49c2-9908-6dc4a39c4716"
-                />
-                <linearGradient
-                  id="97571ef7-1c83-4e06-b701-c2e47e77dca3"
-                  x1="476.94"
-                  y1="156.13"
-                  x2="476.94"
-                  y2="106.68"
-                  gradientTransform="matrix(1, 0, 0, 1, 0, 0)"
-                  xlink:href="#07332201-7176-49c2-9908-6dc4a39c4716"
-                />
-                <linearGradient
-                  id="7d32e13e-a0c7-49c4-af0e-066a2f8cb76e"
-                  x1="666.86"
-                  y1="176.39"
-                  x2="666.86"
-                  y2="117.95"
-                  gradientTransform="matrix(1, 0, 0, 1, 0, 0)"
-                  xlink:href="#07332201-7176-49c2-9908-6dc4a39c4716"
-                />
-              </defs>
-              <title>no data</title>
-              <rect
-                x="317.5"
-                y="142.55"
-                width="437.02"
-                height="603.82"
-                transform="translate(-271.22 62.72) rotate(-12.11)"
-                fill="#e0e0e0"
-              />
-              <g opacity="0.5">
-                <rect
-                  x="324.89"
-                  y="152.76"
-                  width="422.25"
-                  height="583.41"
-                  transform="translate(-271.22 62.72) rotate(-12.11)"
-                  fill="url(#07332201-7176-49c2-9908-6dc4a39c4716)"
-                />
-              </g>
-              <rect
-                x="329.81"
-                y="157.1"
-                width="411.5"
-                height="570.52"
-                transform="translate(-270.79 62.58) rotate(-12.11)"
-                fill="#fafafa"
-              />
-              <rect
-                x="374.18"
-                y="138.6"
-                width="204.14"
-                height="49.45"
-                transform="translate(-213.58 43.93) rotate(-12.11)"
-                fill="url(#0ee1ab3f-7ba2-4205-9d4a-9606ad702253)"
-              />
-              <path
-                d="M460.93,91.9c-15.41,3.31-25.16,18.78-21.77,34.55s18.62,25.89,34,22.58,25.16-18.78,21.77-34.55S476.34,88.59,460.93,91.9ZM470.6,137A16.86,16.86,0,1,1,483.16,117,16.66,16.66,0,0,1,470.6,137Z"
-                transform="translate(-189.92 -59.59)"
-                fill="url(#abca9755-bed1-4a97-b027-7f02ee3ffa09)"
-              />
-              <rect
-                x="375.66"
-                y="136.55"
-                width="199.84"
-                height="47.27"
-                transform="translate(-212.94 43.72) rotate(-12.11)"
-                fill="#6c63ff"
-              />
-              <path
-                d="M460.93,91.9a27.93,27.93,0,1,0,33.17,21.45A27.93,27.93,0,0,0,460.93,91.9ZM470.17,135a16.12,16.12,0,1,1,12.38-19.14A16.12,16.12,0,0,1,470.17,135Z"
-                transform="translate(-189.92 -59.59)"
-                fill="#6c63ff"
-              />
-              <rect
-                x="257.89"
-                y="116.91"
-                width="437.02"
-                height="603.82"
-                fill="#e0e0e0"
-              />
-              <g opacity="0.5">
-                <rect
-                  x="265.28"
-                  y="127.12"
-                  width="422.25"
-                  height="583.41"
-                  fill="url(#2632d424-e666-4ee4-9508-a494957e14ab)"
-                />
-              </g>
-              <rect
-                x="270.65"
-                y="131.42"
-                width="411.5"
-                height="570.52"
-                fill="#fff"
-              />
-              <rect
-                x="374.87"
-                y="106.68"
-                width="204.14"
-                height="49.45"
-                fill="url(#97571ef7-1c83-4e06-b701-c2e47e77dca3)"
-              />
-              <path
-                d="M666.86,118c-15.76,0-28.54,13.08-28.54,29.22s12.78,29.22,28.54,29.22,28.54-13.08,28.54-29.22S682.62,118,666.86,118Zm0,46.08a16.86,16.86,0,1,1,16.46-16.86A16.66,16.66,0,0,1,666.86,164Z"
-                transform="translate(-189.92 -59.59)"
-                fill="url(#7d32e13e-a0c7-49c4-af0e-066a2f8cb76e)"
-              />
-              <rect
-                x="377.02"
-                y="104.56"
-                width="199.84"
-                height="47.27"
-                fill="#6c63ff"
-              />
-              <path
-                d="M666.86,118a27.93,27.93,0,1,0,27.93,27.93A27.93,27.93,0,0,0,666.86,118Zm0,44.05A16.12,16.12,0,1,1,683,145.89,16.12,16.12,0,0,1,666.86,162Z"
-                transform="translate(-189.92 -59.59)"
-                fill="#6c63ff"
-              />
-              <g opacity="0.5">
-                <rect
-                  x="15.27"
-                  y="737.05"
-                  width="3.76"
-                  height="21.33"
-                  fill="#47e6b1"
-                />
-                <rect
-                  x="205.19"
-                  y="796.65"
-                  width="3.76"
-                  height="21.33"
-                  transform="translate(824.47 540.65) rotate(90)"
-                  fill="#47e6b1"
-                />
-              </g>
-              <g opacity="0.5">
-                <rect x="451.49" width="3.76" height="21.33" fill="#47e6b1" />
-                <rect
-                  x="641.4"
-                  y="59.59"
-                  width="3.76"
-                  height="21.33"
-                  transform="translate(523.63 -632.62) rotate(90)"
-                  fill="#47e6b1"
-                />
-              </g>
-              <path
-                d="M961,832.15a4.61,4.61,0,0,1-2.57-5.57,2.22,2.22,0,0,0,.1-.51h0a2.31,2.31,0,0,0-4.15-1.53h0a2.22,2.22,0,0,0-.26.45,4.61,4.61,0,0,1-5.57,2.57,2.22,2.22,0,0,0-.51-.1h0a2.31,2.31,0,0,0-1.53,4.15h0a2.22,2.22,0,0,0,.45.26,4.61,4.61,0,0,1,2.57,5.57,2.22,2.22,0,0,0-.1.51h0a2.31,2.31,0,0,0,4.15,1.53h0a2.22,2.22,0,0,0,.26-.45,4.61,4.61,0,0,1,5.57-2.57,2.22,2.22,0,0,0,.51.1h0a2.31,2.31,0,0,0,1.53-4.15h0A2.22,2.22,0,0,0,961,832.15Z"
-                transform="translate(-189.92 -59.59)"
-                fill="#4d8af0"
-                opacity="0.5"
-              />
-              <path
-                d="M326.59,627.09a4.61,4.61,0,0,1-2.57-5.57,2.22,2.22,0,0,0,.1-.51h0a2.31,2.31,0,0,0-4.15-1.53h0a2.22,2.22,0,0,0-.26.45,4.61,4.61,0,0,1-5.57,2.57,2.22,2.22,0,0,0-.51-.1h0a2.31,2.31,0,0,0-1.53,4.15h0a2.22,2.22,0,0,0,.45.26,4.61,4.61,0,0,1,2.57,5.57,2.22,2.22,0,0,0-.1.51h0a2.31,2.31,0,0,0,4.15,1.53h0a2.22,2.22,0,0,0,.26-.45A4.61,4.61,0,0,1,325,631.4a2.22,2.22,0,0,0,.51.1h0a2.31,2.31,0,0,0,1.53-4.15h0A2.22,2.22,0,0,0,326.59,627.09Z"
-                transform="translate(-189.92 -59.59)"
-                fill="#fdd835"
-                opacity="0.5"
-              />
-              <path
-                d="M855,127.77a4.61,4.61,0,0,1-2.57-5.57,2.22,2.22,0,0,0,.1-.51h0a2.31,2.31,0,0,0-4.15-1.53h0a2.22,2.22,0,0,0-.26.45,4.61,4.61,0,0,1-5.57,2.57,2.22,2.22,0,0,0-.51-.1h0a2.31,2.31,0,0,0-1.53,4.15h0a2.22,2.22,0,0,0,.45.26,4.61,4.61,0,0,1,2.57,5.57,2.22,2.22,0,0,0-.1.51h0a2.31,2.31,0,0,0,4.15,1.53h0a2.22,2.22,0,0,0,.26-.45,4.61,4.61,0,0,1,5.57-2.57,2.22,2.22,0,0,0,.51.1h0a2.31,2.31,0,0,0,1.53-4.15h0A2.22,2.22,0,0,0,855,127.77Z"
-                transform="translate(-189.92 -59.59)"
-                fill="#fdd835"
-                opacity="0.5"
-              />
-              <circle
-                cx="812.64"
-                cy="314.47"
-                r="7.53"
-                fill="#f55f44"
-                opacity="0.5"
-              />
-              <circle
-                cx="230.73"
-                cy="746.65"
-                r="7.53"
-                fill="#f55f44"
-                opacity="0.5"
-              />
-              <circle
-                cx="735.31"
-                cy="477.23"
-                r="7.53"
-                fill="#f55f44"
-                opacity="0.5"
-              />
-              <circle
-                cx="87.14"
-                cy="96.35"
-                r="7.53"
-                fill="#4d8af0"
-                opacity="0.5"
-              />
-              <circle
-                cx="7.53"
-                cy="301.76"
-                r="7.53"
-                fill="#47e6b1"
-                opacity="0.5"
-              />
-            </svg>
-            <span>暂无符合要求的题目</span>
-          </div>
+          <!--页面无数据展示-->
+          <div v-if="data.length <= 0"><no-data-svg></no-data-svg></div>
         </div>
       </div>
     </page-body>
@@ -444,6 +191,7 @@ import {
 import PageHeader from "@/components/PageHeader";
 import PageBody from "@/components/PageBody/index";
 import PageSectionLoading from "@/components/PageBody/PageSectionLoading/index";
+import NoDataSvg from "@/components/NoDataSvg";
 
 const pageLimit = 10;
 
@@ -451,7 +199,8 @@ export default {
   components: {
     PageHeader,
     PageBody,
-    PageSectionLoading
+    PageSectionLoading,
+    NoDataSvg
   },
   computed: {
     ...mapState({

+ 260 - 160
src/views/teacher/Result/index.vue

@@ -15,21 +15,30 @@
         <!--下拉框&搜索框-->
         <div class="form-group">
           <!--下拉框-->
-          <b-field> <a class="button is-link">导出作业成绩</a> </b-field>
           <b-field>
-            <a class="button is-link" @click="isCardModalActive = true"
-              >成绩比例调控</a
+            <a
+              class="button is-link"
+              :disabled="isNoResult"
+              @click="exportStudentResult"
+              >导出作业成绩</a
             >
           </b-field>
+          <b-field>
+            <a
+              class="button is-link"
+              @click="isCardModalActive = true"
+              :disabled="isNoResult"
+              >成绩比例调控
+            </a>
+          </b-field>
         </div>
         <!--成绩图表-->
         <div class="chart">
           <canvas id="result-chart" style="width: 100% !important;"></canvas>
         </div>
-        <!--题目table-->
-        <div class="question-list">
+        <div class="result-list">
           <b-table
-            :data="data"
+            :data="studentFinalResultList"
             paginated
             per-page="50"
             detailed
@@ -43,20 +52,21 @@
                 {{ props.row.user.nickname }}
               </b-table-column>
               <b-table-column label="文档作业" centered sortable>
-                {{ average(props.row.documents) }}
+                {{ addSum(props.row.documents) }}
               </b-table-column>
 
               <b-table-column label="代码作业" centered sortable>
-                {{ average(props.row.codes) }}
+                {{ addSum(props.row.codes) }}
               </b-table-column>
 
               <b-table-column label="互评作业" centered sortable>
-                {{ average(props.row.reviews) }}
+                {{ addSum(props.row.reviews) }}
               </b-table-column>
               <b-table-column
-                field="finalResult"
+                field="finalRes"
                 label="总评"
                 sortable
+                numeric
                 centered
               >
                 <span :class="['tag', type(props.row.finalRes)]">
@@ -112,69 +122,35 @@
       </div>
     </page-body>
     <!-- 成绩比例调控模态框 -->
-    <b-modal :active.sync="isCardModalActive" width="420px" scroll="keep">
+    <b-modal :active.sync="isCardModalActive" scroll="keep">
       <div class="card">
         <div class="title">
           成绩比例调控<small>&nbsp;&nbsp;请按百分比填写</small>
         </div>
         <div class="content">
-          <div class="homework-item">
-            <label>文档作业</label>
-            <div class="homework-percent">
-              <b-field position="is-centered">
-                <b-input placeholder="第一次作业百分比"> </b-input>
-                <p class="control">
-                  <button class="button is-light">%</button>
-                </p>
-              </b-field>
-            </div>
-            <div class="homework-percent">
-              <b-field position="is-centered">
-                <b-input placeholder="第二次作业百分比"> </b-input>
-                <p class="control">
-                  <button class="button is-light">%</button>
-                </p>
-              </b-field>
-            </div>
-            <div class="homework-percent">
-              <b-field
-                :type="$v.percent.$error ? 'is-danger' : ''"
-                :message="$v.percent.$error ? '请填写1-100之间的数字' : ''"
-                position="is-centered"
-              >
+          <b-table
+            :data="resultPercentList"
+            class="customer-table"
+            :striped="true"
+            :narrowed="true"
+          >
+            <template slot-scope="props">
+              <b-table-column field="id" label="作业ID" width="50" numeric>
+                {{ props.row.id }}
+              </b-table-column>
+              <b-table-column field="name" label="作业名称">
+                {{ props.row.name }}
+              </b-table-column>
+              <b-table-column field="type" label="作业类型" email>
+                {{ props.row.type }}
+              </b-table-column>
+              <b-table-column field="percent" label="当前比例">
                 <b-input
-                  v-model="$v.percent.$model"
-                  placeholder="第三次作业百分比"
-                >
-                </b-input>
-                <p class="control">
-                  <button class="button is-light">%</button>
-                </p>
-              </b-field>
-            </div>
-          </div>
-          <div class="homework-item">
-            <label>互评作业</label>
-            <div class="homework-percent">
-              <b-field position="is-centered">
-                <b-input placeholder="第一次作业百分比"> </b-input>
-                <p class="control">
-                  <button class="button is-light">%</button>
-                </p>
-              </b-field>
-            </div>
-          </div>
-          <div class="homework-item">
-            <label>代码作业</label>
-            <div class="homework-percent">
-              <b-field position="is-centered">
-                <b-input placeholder="第一次作业百分比"> </b-input>
-                <p class="control">
-                  <button class="button is-light">%</button>
-                </p>
-              </b-field>
-            </div>
-          </div>
+                  v-model="resultPercentList[props.index].percent"
+                ></b-input>
+              </b-table-column>
+            </template>
+          </b-table>
         </div>
         <a
           :class="['button', 'is-link', { 'is-loading': submitting }]"
@@ -192,8 +168,12 @@ import PageBody from "@/components/PageBody/index";
 import Chart from "chart.js";
 import BInput from "buefy/src/components/input/Input";
 import { required, numeric, between } from "vuelidate/lib/validators";
-import { getStudentFinaleResult } from "@/api/resultstatistics";
+import {
+  getStudentFinaleResult,
+  setResultPercentList
+} from "@/api/resultstatistics";
 import PageSectionLoading from "@/components/PageBody/PageSectionLoading/index";
+import { toExcel } from "@/util/exportExcel";
 
 const RESULT_SECTION = [
   "0分",
@@ -228,33 +208,102 @@ export default {
   },
   data() {
     return {
-      name: "",
-      courses: [],
-      review: [],
+      isNoResult: true,
       data: [],
       isCardModalActive: false,
       myChart2: {},
-      percent: "",
-      finalResultPercentage: [20, 20, 20, 10, 10, 10, 10],
       submitting: false,
-      isServerError: false,
-      errorMessage: "服务器错误,请稍后重试",
       isResultLoading: true,
-      finalResult: [],
-      homeworkNum: 0
+      resultPercentList: [],
+      studentFinalResultList: [],
+      studentResultChartData: [],
+      resultChart: {}
     };
   },
   methods: {
-    //初始化成绩比例
-    initialResultPercent(studentResults) {
-      if (studentResults.length < 1) return;
-      let resultSample = studentResults[0];
-      let homeworkNum = 0;
-      homeworkNum =
-        resultSample.codes.length +
-        resultSample.documents.length +
-        resultSample.reviews.length;
-      this.homeworkNum = homeworkNum;
+    //导出学生作业成绩
+    exportStudentResult() {
+      const th = [
+        "ID",
+        "姓名",
+        "文档作业成绩",
+        "互评作业成绩",
+        "代码作业成绩",
+        "总评"
+      ];
+      // const data = this.studentFinalResultList.map(v => filterVal.map(k => v[k]));
+      // console.log(data);
+      let data = [];
+      for (let item of this.studentFinalResultList) {
+        let docResString = "";
+        let reviewResString = "";
+        let codeResString = "";
+        for (let docRes of item.documents) {
+          docResString += `${docRes.name}:${docRes.score};`;
+        }
+        for (let reviewRes of item.reviews) {
+          reviewResString += `${reviewRes.name}:${reviewRes.score}`;
+        }
+        for (let codeRes of item.codes) {
+          codeResString += `${codeRes.name}:${codeRes.score}`;
+        }
+        let outPutInfo = [
+          item.id,
+          item.user.nickname,
+          docResString,
+          reviewResString,
+          codeResString,
+          item.finalRes
+        ];
+        data.push(outPutInfo);
+      }
+      console.log(data);
+      const [fileName, fileType, sheetName] = ["学生成绩", "xlsx", "成绩"];
+      toExcel({ th, data, fileName, fileType, sheetName });
+    },
+    //绘制成绩图表
+    drawResultChart() {
+      let myChart2 = new Chart(this.resultChart, {
+        type: "bar",
+        data: {
+          labels: RESULT_SECTION,
+          datasets: [
+            {
+              label: "人数",
+              backgroundColor: "rgba(54,162,235,0.3)",
+              borderColor: "rgba(54,162,235,1)",
+              borderWidth: 1,
+              pointStrokeColor: "#fff",
+              pointStyle: "crossRot",
+              data: this.studentResultChartData,
+              cubicInterpolationMode: "monotone",
+              spanGaps: "false",
+              fill: "false"
+            }
+          ]
+        },
+        options: {}
+      });
+      this.myChart2 = myChart2;
+    },
+    //生成成绩百分比
+    createResultPercentList() {
+      let resultPercentList = [];
+      let resultData = this.data[0];
+      for (let codesHw of resultData.codes) {
+        codesHw.type = "CODE";
+        resultPercentList.push(codesHw);
+      }
+      for (let documentsHw of resultData.documents) {
+        documentsHw.type = "DOCUMENT";
+        resultPercentList.push(documentsHw);
+      }
+      for (let reviewHw of resultData.reviews) {
+        reviewHw.type = "REVIEW";
+        resultPercentList.push(reviewHw);
+      }
+      this.resultPercentList = resultPercentList;
+      console.log(resultPercentList);
     },
     //统计学生成绩分段
     studenResultStatistics(studentResults) {
@@ -280,7 +329,8 @@ export default {
         }
         resultStatisticsData.push(count);
       }
-      return resultStatisticsData;
+      this.studentResultChartData = resultStatisticsData;
+      // return resultStatisticsData;
     },
     type(value) {
       const number = parseFloat(value);
@@ -292,55 +342,117 @@ export default {
         return "is-success";
       }
     },
-    //计算平均成绩
-    average(resultList) {
-      if (resultList.length < 1) {
-        return "--";
-      }
-      let res = 0;
+    //计算每种作业平均成绩
+    addSum(resultList) {
+      let finalRes = 0;
       for (let item of resultList) {
-        res += item.score;
+        finalRes += item.calScore;
       }
-      return res / resultList.length;
+      return finalRes;
     },
-    //计算总评
-    getFinalResult(resultList) {
-      console.log(resultList);
-      let finalResultList = [];
-      for (let i = 0; i < resultList.length; i++) {
+    //计算学生总评
+    getFinalResult(studentResultList) {
+      let studentFinalResultList = [];
+      for (let i = 0; i < studentResultList.length; i++) {
+        let studentRes = studentResultList[i];
+        let resultPercentList = this.resultPercentList;
+        let index = 0;
+        let codesResList = [];
+        let docsResList = [];
+        let reviewsResList = [];
         let finalRes = 0;
-        let result = resultList[i];
-        console.log(result);
-        //let currentIndex = 0;
-        for (let docRes of result.documents) {
-          finalRes += docRes.score;
-          // finalRes +=
-          //   docRes.score * (this.finalResultPercentage[currentIndex] / 100);
-          // currentIndex++;
+        for (let codeRes of studentRes.codes) {
+          //根据成绩比例计算分数
+          codeRes.calScore = codeRes.score
+            ? (codeRes.score * resultPercentList[index].percent) / 100
+            : 0;
+          // console.log(codeRes.calScore);
+          codesResList.push(codeRes);
+          finalRes += codeRes.calScore;
+          index++;
         }
-        for (let codeRes of result.codes) {
-          finalRes += codeRes.score;
-          // finalRes +=
-          //   codeRes.score * (this.finalResultPercentage[currentIndex] / 100);
-          // currentIndex++;
+        studentRes.codes = codesResList;
+        for (let docRes of studentRes.documents) {
+          docRes.calScore = docRes.score
+            ? (docRes.score * resultPercentList[index].percent) / 100
+            : 0;
+          docsResList.push(docRes);
+          finalRes += docRes.calScore;
+          index++;
         }
-        for (let reviewRes of result.reviews) {
-          finalRes += reviewRes.score;
-          // finalRes +=
-          //   reviewRes.score * (this.finalResultPercentage[currentIndex] / 100);
-          // currentIndex++;
+        studentRes.documents = docsResList;
+        for (let reviewRes of studentRes.reviews) {
+          reviewRes.calScore = reviewRes.score
+            ? (reviewRes.score * resultPercentList[index].percent) / 100
+            : 0;
+          reviewsResList.push(reviewRes);
+          finalRes += reviewRes.calScore;
+          index++;
         }
-        result.finalRes = parseInt(finalRes / this.homeworkNum);
-        finalResultList.push(result);
+        studentRes.reviews = reviewsResList;
+        studentRes.finalRes = finalRes;
+        studentFinalResultList.push(studentRes);
       }
-      this.data = finalResultList;
+      this.studentFinalResultList = studentFinalResultList;
     },
+    //调整成绩比例
     confirmPercent() {
-      this.submitting = true;
-      this.$toast.open({
-        message: "成绩比例调整成功",
-        type: "is-success"
-      });
+      //检查是否满足100%
+      let percentSum = 0;
+      let docPercentList = [];
+      let codePercentList = [];
+      let reviewPercentList = [];
+      for (let item of this.resultPercentList) {
+        percentSum += parseInt(item.percent);
+        switch (item.type) {
+          case "DOCUMENT":
+            docPercentList.push({ id: item.id, percent: item.percent });
+            break;
+          case "CODE":
+            codePercentList.push({ id: item.id, percent: item.percent });
+            break;
+          case "REVIEW":
+            reviewPercentList.push({ id: item.id, percent: item.percent });
+            break;
+        }
+      }
+      if (percentSum != 100) {
+        this.$toast.open({
+          message: "请使比例加和为100%",
+          type: "is-warning"
+        });
+      } else {
+        //提交成绩修改结果
+        this.submitting = true;
+        setResultPercentList(
+          this.$route.params.courseId,
+          docPercentList,
+          codePercentList,
+          reviewPercentList
+        )
+          .then(res => {
+            console.log(res);
+            this.$toast.open({
+              message: "成绩比例调整成功",
+              type: "is-success"
+            });
+            this.isCardModalActive = false;
+            this.submitting = false;
+            //重新计算成绩比例
+            this.getFinalResult(this.data);
+            this.studenResultStatistics(this.studentFinalResultList);
+            let myChart2 = this.myChart2;
+            console.log(myChart2);
+            myChart2.data.datasets[0].data = this.studentResultChartData;
+            myChart2.update();
+          })
+          .catch(e => {
+            this.$toast.open({
+              message: e.message || "服务器未知错误",
+              type: "is-danger"
+            });
+          });
+      }
     }
   },
   async mounted() {
@@ -350,38 +462,26 @@ export default {
     );
     this.isResultLoading = false;
     this.data = resultData.data;
-    console.log(this.data);
-    //初始化默认成绩比例
-    this.initialResultPercent(this.data);
-    //统计学生总评
-    console.log(resultData.data);
-    this.getFinalResult(resultData.data);
-    //统计学生分数
-    let studentResultStatistics = this.studenResultStatistics(this.data);
-    //绘制图表
-    let resultChart = document.getElementById("result-chart");
-    let myChart2 = new Chart(resultChart, {
-      type: "bar",
-      data: {
-        labels: RESULT_SECTION,
-        datasets: [
-          {
-            label: "人数",
-            backgroundColor: "rgba(54,162,235,0.3)",
-            borderColor: "rgba(54,162,235,1)",
-            borderWidth: 1,
-            pointStrokeColor: "#fff",
-            pointStyle: "crossRot",
-            data: studentResultStatistics,
-            cubicInterpolationMode: "monotone",
-            spanGaps: "false",
-            fill: "false"
-          }
-        ]
-      },
-      options: {}
-    });
-    this.myChart2 = myChart2;
+    if (resultData.data && resultData.data.length > 0) {
+      this.isNoResult = false;
+      // console.log(this.data);
+      //初始化默认成绩比例
+      // this.initialResultPercent(this.data);
+      //初始化各作业比例
+      this.createResultPercentList();
+      //统计学生总评
+      this.getFinalResult(resultData.data);
+      //统计学生分数
+      this.studenResultStatistics(this.data);
+      //绘制图表
+      let resultChart = document.getElementById("result-chart");
+      this.resultChart = resultChart;
+      this.drawResultChart();
+    } else {
+      let resultChart = document.getElementById("result-chart");
+      this.resultChart = resultChart;
+      this.drawResultChart();
+    }
   }
 };
 </script>
@@ -393,7 +493,7 @@ export default {
 .chart {
   margin-top: 1.5rem;
 }
-.question-list {
+.result-list {
   margin-top: 3rem;
 }
 .form-group {

+ 1 - 1
src/views/teacher/Review/ReviewDetail/GroupArgueDetail/index.vue

@@ -45,7 +45,7 @@
             >
               提 交
             </div>
-            <check-list v-model="detail.checkList" />
+            <check-list :isTeacher="true" v-model="detail.checkList" />
           </div>
         </div>
       </div>

+ 19 - 1
src/views/teacher/Review/index.vue

@@ -35,6 +35,10 @@
           </router-link>
         </div>
       </div>
+      <no-data-svg
+        style="margin-top: 20px;"
+        v-if="!reviewListLoading && !(review && review.length > 0)"
+      ></no-data-svg>
     </page-body>
     <b-modal :active.sync="showModal">
       <div class="card">
@@ -138,12 +142,14 @@ import ReviewStateTag from "@/components/ReviewStateTag/index";
 import { getDocAssignmentListTeacherByCrs } from "@/api/assignment";
 import timeMixins from "@/mixins/time";
 import { required } from "vuelidate/lib/validators";
+import NoDataSvg from "@/components/NoDataSvg";
 export default {
   components: {
     ReviewStateTag,
     PageBody,
     PageHeader,
-    Card
+    Card,
+    NoDataSvg
   },
   computed: {
     ...mapState({
@@ -253,12 +259,24 @@ export default {
         this.adjustMaxDate("reviewDdlConfig", newValue);
         this.adjustMaxDate("argueDdlConfig", newValue);
       }
+    },
+    documentHwId(newValue) {
+      if (newValue) {
+        const selectedDoc = this.docHomework.find(
+          doc => String(doc.id) === String(newValue)
+        );
+        if (selectedDoc) {
+          this.startAtConfig.minDate = String(selectedDoc.startAt);
+        }
+      }
     }
   },
   methods: {
     async getReviewList() {
       const { courseId } = this.$route.params;
+      this.reviewListLoading = true;
       const { data } = await getTeacherCourseReviewList(courseId);
+      this.reviewListLoading = false;
       this.review = data;
     },
     async getDocHomework() {

+ 70 - 5
yarn.lock

@@ -1093,6 +1093,13 @@ address@^1.0.3:
   version "1.0.3"
   resolved "https://registry.npmjs.org/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
 
+adler-32@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25"
+  dependencies:
+    exit-on-epipe "~1.0.1"
+    printj "~1.1.0"
+
 agent-base@2:
   version "2.1.1"
   resolved "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz#d6de10d5af6132d5bd692427d46fc538539094c7"
@@ -1997,6 +2004,15 @@ caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
 
+cfb@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.1.0.tgz#44fb1b30eee014fa5633a0ed5f26c87fd765799a"
+  dependencies:
+    adler-32 "~1.2.0"
+    commander "^2.16.0"
+    crc-32 "~1.2.0"
+    printj "~1.1.2"
+
 chai-nightwatch@~0.1.x:
   version "0.1.1"
   resolved "http://registry.npmjs.org/chai-nightwatch/-/chai-nightwatch-0.1.1.tgz#1ca56de768d3c0868fe7fc2f4d32c2fe894e6be9"
@@ -2204,6 +2220,13 @@ code-point-at@^1.0.0:
   version "1.1.0"
   resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
+codepage@~1.14.0:
+  version "1.14.0"
+  resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99"
+  dependencies:
+    commander "~2.14.1"
+    exit-on-epipe "~1.0.1"
+
 collection-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -2285,9 +2308,13 @@ commander@2.9.0:
   dependencies:
     graceful-readlink ">= 1.0.0"
 
-commander@^2.14.1, commander@^2.18.0, commander@^2.19.0, commander@^2.9.0:
+commander@^2.14.1, commander@^2.16.0, commander@^2.18.0, commander@^2.19.0, commander@^2.9.0:
   version "2.19.0"
-  resolved "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
+
+commander@~2.14.1:
+  version "2.14.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
 
 common-tags@^1.4.0:
   version "1.8.0"
@@ -2454,6 +2481,13 @@ cosmiconfig@^5.0.0, cosmiconfig@^5.0.2:
     js-yaml "^3.9.0"
     parse-json "^4.0.0"
 
+crc-32@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208"
+  dependencies:
+    exit-on-epipe "~1.0.1"
+    printj "~1.1.0"
+
 create-ecdh@^4.0.0:
   version "4.0.3"
   resolved "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff"
@@ -3497,6 +3531,10 @@ execa@^1.0.0:
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
+exit-on-epipe@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
+
 exit@^0.1.2:
   version "0.1.2"
   resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@@ -3751,6 +3789,10 @@ file-loader@^2.0.0:
     loader-utils "^1.0.2"
     schema-utils "^1.0.0"
 
+file-saver@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.1.tgz#7fe2242af1cbc559a29d8176078a8b56d781fa79"
+
 file-uri-to-path@1:
   version "1.0.0"
   resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -3867,7 +3909,6 @@ flat-cache@^1.2.1:
 flatpickr@^4.5.1:
   version "4.5.2"
   resolved "http://registry.npm.taobao.org/flatpickr/download/flatpickr-4.5.2.tgz#47c8ad472a096e5fb7e74b809b0703535383f20d"
-  integrity sha1-R8itRyoJbl+350uAmwcDU1OD8g0=
 
 flush-write-stream@^1.0.0:
   version "1.0.3"
@@ -3918,6 +3959,10 @@ forwarded@~0.1.2:
   version "0.1.2"
   resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
 
+frac@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b"
+
 fragment-cache@^0.2.1:
   version "0.2.1"
   resolved "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
@@ -7537,6 +7582,10 @@ pretty@2.0.0:
     extend-shallow "^2.0.1"
     js-beautify "^1.6.12"
 
+printj@~1.1.0, printj@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
+
 private@^0.1.6, private@^0.1.8:
   version "0.1.8"
   resolved "https://registry.npmjs.org/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -8504,6 +8553,12 @@ sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "http://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
 
+ssf@~0.10.2:
+  version "0.10.2"
+  resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.10.2.tgz#65b2b4fcdfd967bc8e8383a41349009893115976"
+  dependencies:
+    frac "~1.1.2"
+
 sshpk@^1.7.0:
   version "1.15.2"
   resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629"
@@ -9301,7 +9356,6 @@ vue-eslint-parser@^4.0.2:
 vue-flatpickr-component@^8.1.1:
   version "8.1.1"
   resolved "http://registry.npm.taobao.org/vue-flatpickr-component/download/vue-flatpickr-component-8.1.1.tgz#f0876e62cc8a6fdbe0e0cc5068f3a314b972a96f"
-  integrity sha1-8IduYsyKb9vg4MxQaPOjFLlyqW8=
   dependencies:
     flatpickr "^4.5.1"
 
@@ -9337,7 +9391,6 @@ vue-loader@^15.4.2:
 vue-markdown@^2.2.4:
   version "2.2.4"
   resolved "http://registry.npm.taobao.org/vue-markdown/download/vue-markdown-2.2.4.tgz#db0f774178f3bc91ee18c626d86a3a0d2de22746"
-  integrity sha1-2w93QXjzvJHuGMYm2Go6DS3iJ0Y=
   dependencies:
     highlight.js "^9.12.0"
     markdown-it "^6.0.1"
@@ -9797,6 +9850,18 @@ ws@^6.0.0:
   dependencies:
     async-limiter "~1.0.0"
 
+xlsx@^0.14.1:
+  version "0.14.1"
+  resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.14.1.tgz#ecedf536bd1e94055486ff484f5888dcaa89bd3d"
+  dependencies:
+    adler-32 "~1.2.0"
+    cfb "^1.1.0"
+    codepage "~1.14.0"
+    commander "~2.17.1"
+    crc-32 "~1.2.0"
+    exit-on-epipe "~1.0.1"
+    ssf "~0.10.2"
+
 xml-name-validator@^3.0.0:
   version "3.0.0"
   resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików