Przeglądaj źródła

Merge branch 'develop' into feature/assignment-zhangyasen

Johnson 7 lat temu
rodzic
commit
b0cb0bf187

+ 10 - 0
mock/modules/user/index.js

@@ -22,4 +22,14 @@ router.route("/:userId").post((req, res) => {
   });
 });
 
+router.route("/:userId/password").post((req, res) => {
+  //修改用户密码
+  res.send({
+    data: userSerializer(),
+    abilities: {
+      update: true
+    }
+  });
+});
+
 module.exports = router;

+ 3 - 1
src/App.vue

@@ -3,7 +3,9 @@
 </template>
 
 <style lang="scss">
+@import "assets/scss/app";
+
 #app {
-  background: #f4f7fb;
+  background: $default-background;
 }
 </style>

BIN
src/assets/icon/edit.png


BIN
src/assets/icon/file.png


BIN
src/assets/icon/home.png


BIN
src/assets/icon/laptop.png


BIN
src/assets/icon/linechart.png


BIN
src/assets/icon/question.png


BIN
src/assets/icon/team.png


BIN
src/assets/icon/work.png


+ 3 - 1
src/assets/scss/_variables.scss

@@ -77,6 +77,8 @@ $variable-columns: true !default;
 
 $primary: $turquoise !default;
 
+$primary: #7060ff;
+
 $info: $cyan !default;
 $success: $green !default;
 $warning: $yellow !default;
@@ -128,7 +130,7 @@ $pre-background: $background !default;
 
 // Link colors
 
-$link: $blue !default;
+$link: #6172ec !default;
 $link-invert: $blue-invert !default;
 $link-visited: $purple !default;
 

+ 46 - 3
src/assets/scss/app.scss

@@ -8,8 +8,19 @@
 @import "~buefy/src/scss/buefy";
 
 
-$border-color:#b9c7d8;
-$default-border:1px solid #c9d8ea;
+$default-shadow:0 3px 8px rgba(211, 215, 222, 0.4);
+$default-shadow-lighter:0 3px 8px rgba(211, 215, 222, 0.2);
+
+$default-radius:4px;
+
+$default-background:#f4f7fc;
+
+$default-border-color:rgba(84, 66, 251, 0.56);
+$default-border:2px solid $default-border-color;
+
+$default-margin:0.75rem;
+$default-margin-between:1.5rem;
+
 
 .has-text-primary{
   color:#209cee !important;
@@ -17,4 +28,36 @@ $default-border:1px solid #c9d8ea;
 
 .input{
   box-shadow: none !important;
-}
+}
+
+
+.menu.dark-menu{
+
+  .menu-label {
+    color: rgba(255, 255, 255, 0.79);
+    font-size: 1rem;
+    padding-bottom: 0 !important;
+    font-weight: bold;
+  }
+  .menu-list {
+    font-weight: bold;
+    font-size: 1.1rem;
+    li {
+      margin-bottom: 10px;
+      a {
+        display: flex;
+        color: #ffffff;
+        &:hover {
+          background-color: rgba(0, 0, 0, 0.18);
+        }
+      }
+      img {
+        height: 20px;
+        margin-right: 5px;
+      }
+    }
+  }
+  .is-active {
+    background-color: rgba(0, 0, 0, 0.18) !important;
+  }
+}

BIN
src/assets/sideBar.png


BIN
src/assets/white.png


+ 5 - 5
src/components/Card/index.vue

@@ -25,16 +25,16 @@ export default {
 
 <style lang="scss" scoped>
 @import "../../assets/scss/app";
-
 .custom-card {
   width: 100%;
   height: 100%;
   position: relative;
   background: #ffffff;
-  border: $default-border;
+  box-shadow: $default-shadow;
+  border-radius: $default-radius;
   color: #152935;
+  border: $default-border;
 }
-
 .hoverable {
   cursor: pointer;
   position: relative;
@@ -51,12 +51,12 @@ export default {
   top: -5px;
   left: -5px;
   padding: 5px;
-  box-shadow: 0 0 0 4px #8897a2;
+  border-radius: 5px;
+  box-shadow: 0 0 0 4px $default-border-color;
   transition: transform 0.2s, opacity 0.2s;
   transform: scaleY(0.9) scaleX(0.95);
   opacity: 0;
 }
-
 .hoverable:hover:after {
   transform: scale(1);
   opacity: 1;

+ 16 - 0
src/components/DefaultHeader/index.vue

@@ -0,0 +1,16 @@
+<template>
+  <nav class="navbar nav-section"><slot></slot></nav>
+</template>
+
+<style lang="scss" scoped>
+@import "../../assets/scss/app";
+
+.nav-section {
+  width: 100%;
+  box-shadow: $default-shadow-lighter;
+  border-bottom: $default-border;
+}
+.navbar {
+  height: 4.25rem;
+}
+</style>

+ 11 - 0
src/components/PageHeader/index.jsx

@@ -0,0 +1,11 @@
+export default {
+  props: {
+    routes: {
+      type: Array,
+      default: () => []
+    }
+  },
+  render() {
+    return <h1>hello</h1>;
+  }
+};

+ 0 - 0
src/components/PageHeader/index.scss


+ 53 - 0
src/components/UserDropdown/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <b-dropdown position="is-bottom-left">
+    <a class="navbar-item" slot="trigger">
+      <span>{{ name }}</span>
+      <b-icon icon="menu-down"></b-icon>
+    </a>
+
+    <b-dropdown-item custom>
+      以 <b>{{ name }}</b> 的身份登录
+    </b-dropdown-item>
+    <hr class="dropdown-divider" />
+    <b-dropdown-item has-link value="settings">
+      <router-link :to="`${prefix}/profile`">
+        <b-icon icon="settings"></b-icon>
+        个人设置
+      </router-link>
+    </b-dropdown-item>
+    <b-dropdown-item has-link value="logout">
+      <router-link :to="{ name: 'LOGIN' }">
+        <b-icon icon="logout"></b-icon>
+        登出
+      </router-link>
+    </b-dropdown-item>
+  </b-dropdown>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import { FETCH_PROFILE } from "@/store/type/actions.type";
+
+export default {
+  computed: {
+    ...mapState({
+      profile: state => state.user.profile
+    }),
+    name() {
+      const { nickname, username } = this.profile;
+      return nickname ? nickname : username;
+    },
+    role() {
+      const { role } = this.profile;
+      return role;
+    },
+    prefix() {
+      const { role = "" } = this;
+      return `/${role.toLowerCase()}/setting`;
+    }
+  },
+  async mounted() {
+    await this.$store.dispatch(FETCH_PROFILE, this.profile.id);
+  }
+};
+</script>

+ 14 - 34
src/layouts/HomeLayout/HomeHeader/index.vue

@@ -1,44 +1,30 @@
 <template>
-  <nav class="navbar nav-section">
+  <default-header>
     <div class="container">
       <div class="navbar-brand">
-        <a class="navbar-item header-logo"> MOOC </a>
+        <a class="navbar-item header-logo"> SEEC </a>
         <a class="navbar-item"><b-icon pack="fab" icon="github"></b-icon></a>
         <a class="navbar-item"><b-icon pack="fab" icon="twitter"></b-icon></a>
       </div>
 
       <div class="navbar-menu">
-        <div class="navbar-end">
-          <b-dropdown position="is-bottom-left">
-            <a class="navbar-item" slot="trigger">
-              <span>{{ name }}</span>
-              <b-icon icon="menu-down"></b-icon>
-            </a>
-
-            <b-dropdown-item custom>
-              以 <b>{{ name }}</b> 的身份登录
-            </b-dropdown-item>
-            <hr class="dropdown-divider" />
-            <b-dropdown-item value="settings">
-              <b-icon icon="settings"></b-icon>
-              个人设置
-            </b-dropdown-item>
-            <b-dropdown-item value="logout">
-              <b-icon icon="logout"></b-icon>
-              登出
-            </b-dropdown-item>
-          </b-dropdown>
-        </div>
+        <div class="navbar-end"><user-dropdown /></div>
       </div>
     </div>
-  </nav>
+  </default-header>
 </template>
 
 <script>
 import { mapState } from "vuex";
 import { FETCH_PROFILE } from "@/store/type/actions.type";
+import DefaultHeader from "@/components/DefaultHeader";
+import UserDropdown from "@/components/UserDropdown";
 
 export default {
+  components: {
+    DefaultHeader,
+    UserDropdown
+  },
   computed: {
     ...mapState({
       profile: state => state.user.profile
@@ -57,22 +43,16 @@ export default {
 <style lang="scss" scoped>
 @import "../../../assets/scss/app";
 
-.nav-section {
-  border-bottom: $default-border;
-}
-
-@media screen and (min-width: 1088px) {
-  .navbar {
-    min-height: 4.25rem;
-  }
-}
 .header-logo {
   font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
     Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial,
     sans-serif;
   font-weight: bold;
   font-size: 1.7em;
-  color: #3d70b2;
+  text-align: center;
+  background: -webkit-linear-gradient(right, #82b1ed, #b073fd); /* 背景色渐变 */
+  -webkit-background-clip: text; /* 规定背景的划分区域 */
+  -webkit-text-fill-color: transparent;
   padding-right: 20px;
   a {
     color: #3d70b2;

+ 1 - 1
src/layouts/LoginLayout.vue

@@ -1,7 +1,7 @@
 <template>
   <section class="main-body">
     <section class="main-body-section">
-      <div class="logo">欢迎使用 MOOC</div>
+      <div class="logo">欢迎使用 SEEC 系统</div>
       <div class="login_container">
         <div class="container__main"><router-view /></div>
       </div>

+ 65 - 0
src/layouts/SettingLayout.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="section">
+    <div class="container">
+      <div class="columns is-desktop">
+        <div class="column is-3">
+          <aside class="menu">
+            <ul class="menu-list">
+              <li>
+                <router-link active-class="is-active" :to="`${prefix}/profile`">
+                  个人设置
+                </router-link>
+              </li>
+              <li>
+                <router-link
+                  active-class="is-active"
+                  :to="`${prefix}/password`"
+                >
+                  密码设置
+                </router-link>
+              </li>
+            </ul>
+          </aside>
+        </div>
+        <div class="column is-9">
+          <div class="router-part"><router-view /></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import { FETCH_PROFILE } from "@/store/type/actions.type";
+
+export default {
+  computed: {
+    ...mapState({
+      profile: state => state.user.profile
+    }),
+    role() {
+      const { role } = this.profile;
+      return role;
+    },
+    prefix() {
+      const { role = "" } = this;
+      return `/${role.toLowerCase()}/setting`;
+    }
+  },
+  async mounted() {
+    await this.$store.dispatch(FETCH_PROFILE, this.profile.id);
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../assets/scss/app";
+.router-part {
+  background: white;
+  padding: $default-margin-between;
+  border-radius: $default-radius;
+  box-shadow: $default-shadow;
+  border: $default-border;
+}
+</style>

+ 80 - 27
src/layouts/courseLayouts/LayoutStructure.vue

@@ -1,16 +1,30 @@
 <template>
-  <div class="course-layout">
-    <div class="menu-section">
-      <div class="menu-sticky">
-        <div class="header-logo">MOOC</div>
-        <div class="course-detail">
-          {{ course.name }}
-          <b-loading :is-full-page="false" :active.sync="isLoading"></b-loading>
+  <div class="course">
+    <div class="course-layout">
+      <div class="menu-section">
+        <div class="menu-sticky">
+          <div class="header-logo">SEEC</div>
+          <div class="course-detail">
+            {{ course.name }}
+            <b-loading
+              :is-full-page="false"
+              :active.sync="isLoading"
+            ></b-loading>
+          </div>
+          <slot />
         </div>
-        <slot />
+      </div>
+      <div class="router-section">
+        <default-header
+          style="height: 4.25rem;border-bottom: 2px solid rgba(84, 66, 251, 0.16);"
+        >
+          <div class="navbar-menu">
+            <div class="navbar-end nav-user"><user-dropdown /></div>
+          </div>
+        </default-header>
+        <div class="router-part"><router-view /></div>
       </div>
     </div>
-    <div class="router-section"><router-view /></div>
   </div>
 </template>
 
@@ -18,7 +32,14 @@
 import { FETCH_CURRENT_COURSE } from "@/store/type/actions.type";
 import { mapState } from "vuex";
 
+import DefaultHeader from "@/components/DefaultHeader";
+import UserDropdown from "@/components/UserDropdown";
+
 export default {
+  components: {
+    DefaultHeader,
+    UserDropdown
+  },
   computed: {
     ...mapState({
       course: state => state.course.currentCourse
@@ -44,7 +65,8 @@ export default {
 $menu-width: 300px;
 $menu-padding: 24px;
 $screen-width: 1450px;
-$left-padding: "(100% -" #{$screen-width} "-" #{$menu-padding} "* 2) / 2";
+//$left-padding: "(100% -" #{$screen-width} "-" #{$menu-padding} "* 2) / 2";
+$left-padding: 0;
 $left-width: #{$left-padding} + #{$menu-width};
 $left-padding-calc: calc(#{$left-padding});
 $left-width-calc: calc(#{$left-width});
@@ -58,33 +80,53 @@ $top-section-height: 4.25rem;
 }
 
 .router-section {
-  padding: 24px;
+  /*padding: 24px;*/
   flex-grow: 1;
-  max-width: 1000px;
+  flex: 1;
+  /*max-width: 1000px;*/
 }
 .menu-section {
-  min-width: $menu-width;
-  width: $left-width-calc;
-  background: white;
-  border-right: 1px solid #c9d8ea;
+  //min-width: $menu-width;
+  //width: $left-width-calc;
+  width: 300px;
+  /*background: white;*/
+  /*border-right: 3px solid rgb(159, 148, 254);*/
   padding-left: $left-padding-calc;
+  box-shadow: $default-shadow;
+  z-index: 31;
+  /*TODO: 寻找更好的图片或背景颜色*/
+  /*background-image: url("../../assets/sideBar.png");*/
+  /*background-image: linear-gradient(to top, #493a9e 0%, #4f54e9 100%);*/
+  /*background: linear-gradient(to top, #7c71bd 0%, #4f62e9 100%);*/
+  background: linear-gradient(to top, #7c71bdc7 0%, #6172ec 100%);
+  background-size: cover;
+  position: sticky;
+  overflow-y: auto;
+  overflow-x: hidden;
+  height: 100vh;
+  top: 0;
   .menu-sticky {
-    width: $menu-width;
-    position: sticky;
-    overflow-y: auto;
     padding: 0 $menu-padding;
-    top: 0;
-    height: 100vh;
-
+    height: 100%;
+    width: 100%;
+    /*min-width: 300px;*/
+    //width: $menu-width;
     .header-logo {
       font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont,
         "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue",
         Helvetica, Arial, sans-serif;
       font-weight: bold;
       font-size: 1.7em;
-      color: #3d70b2;
+      color: #ffffff;
       padding-right: 20px;
+      height: 4.25rem;
       line-height: $top-section-height;
+      margin: 0 -24px;
+      /* background: black; */
+      background-color: transparent;
+      border-bottom: 2px solid rgba(255, 255, 255, 0.16);
+      /*background-image: linear-gradient(to left, #4f62e9 0%, #161aa7 100%);*/
+      padding-left: 24px;
       a {
         color: #3d70b2;
       }
@@ -92,13 +134,24 @@ $top-section-height: 4.25rem;
     .course-detail {
       position: relative;
       padding: 15px;
-      background: #f4f7fb;
-      border: 1px solid #c9d8ea;
+      font-weight: bold;
+      background: white;
       border-radius: 3px;
-      box-shadow: 0 1px 2px rgba(201, 216, 234, 0.5);
-      margin-bottom: 1em;
+      box-shadow: 0 1px 2px #4a4a4a;
+      margin: $default-margin-between 0;
       min-height: 54px;
     }
+    .link {
+      color: #ffffff;
+      font-weight: bold;
+    }
   }
 }
+.nav-user {
+  margin-right: $default-margin;
+}
+
+.router-part {
+  padding: $default-margin-between;
+}
 </style>

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

@@ -1,10 +1,10 @@
 <template>
-  <aside class="menu">
+  <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>
     </ul>
@@ -12,17 +12,17 @@
     <ul class="menu-list">
       <li>
         <router-link active-class="is-active" :to="`${prefix}/code`">
-          代码作业
+          <img alt="" src="../../assets/icon/laptop.png" />代码作业
         </router-link>
       </li>
       <li>
         <router-link active-class="is-active" :to="`${prefix}/document`"
-          >文档作业</router-link
+          ><img alt="" src="../../assets/icon/file.png" />文档作业</router-link
         >
       </li>
       <li>
         <router-link active-class="is-active" :to="`${prefix}/review`"
-          >评审作业</router-link
+          ><img alt="" src="../../assets/icon/edit.png" />评审作业</router-link
         >
       </li>
     </ul>
@@ -30,7 +30,7 @@
     <ul class="menu-list">
       <li>
         <router-link active-class="is-active" :to="`${prefix}/group`"
-          >我的小组</router-link
+          ><img alt="" src="../../assets/icon/team.png" />我的小组</router-link
         >
       </li>
     </ul>

+ 58 - 13
src/layouts/courseLayouts/TeacherMenu.vue

@@ -1,29 +1,44 @@
 <template>
-  <aside class="menu">
+  <aside class="menu dark-menu">
     <p class="menu-label">总览</p>
     <ul class="menu-list">
       <li>
-        <router-link active-class="is-active" :to="`${prefix}`"
-          >工作台</router-link
-        >
+        <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" />课程选择
+        </router-link>
+      </li>
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/question/`">
+          <img alt="" src="../../assets/icon/question.png" />题目管理
+        </router-link>
       </li>
     </ul>
     <p class="menu-label">作业</p>
     <ul class="menu-list">
       <li>
-        <router-link active-class="is-active" :to="`${prefix}/code`"
-          >代码作业</router-link
-        >
+        <router-link active-class="is-active" :to="`${prefix}/code`">
+          <img alt="" src="../../assets/icon/laptop.png" />代码作业
+        </router-link>
+      </li>
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/document`">
+          <img alt="" src="../../assets/icon/file.png" />文档作业
+        </router-link>
       </li>
       <li>
-        <router-link active-class="is-active" :to="`${prefix}/document`"
-          >文档作业</router-link
-        >
+        <router-link active-class="is-active" :to="`${prefix}/review`">
+          <img alt="" src="../../assets/icon/edit.png" />评审作业
+        </router-link>
       </li>
       <li>
-        <router-link active-class="is-active" :to="`${prefix}/review`"
-          >评审作业</router-link
-        >
+        <router-link active-class="is-active" :to="`${prefix}/result`">
+          <img alt="" src="../../assets/icon/linechart.png" />成绩管理
+        </router-link>
       </li>
     </ul>
   </aside>
@@ -38,3 +53,33 @@ export default {
   }
 };
 </script>
+<style lang="scss" scoped>
+// .menu-label {
+//   color: #ffffff;
+//   font-size: 1rem;
+//   padding-bottom: 5px;
+//   border-bottom: 1px solid rgba(255, 255, 255, 0.24);
+//   font-weight: bold;
+// }
+// .menu-list {
+//   font-weight: bold;
+//   font-size: 1.1rem;
+//   li {
+//     margin-bottom: 10px;
+//     a {
+//       display: flex;
+//       color: #ffffff;
+//       &:hover {
+//         background-color: rgba(255, 255, 255, 0.24);
+//       }
+//     }
+//     img {
+//       height: 20px;
+//       margin-right: 5px;
+//     }
+//   }
+// }
+// .is-active {
+//   background-color: rgba(255, 255, 255, 0.24) !important;
+// }
+</style>

+ 40 - 4
src/router/routes.js

@@ -31,31 +31,53 @@ export default [
   {
     path: "/teacher",
     component: () => import("@/layouts/HomeLayout"),
-    meta: { requiresAuth: true },
+    meta: { requiresAuth: false },
     children: [
       {
         name: TEACHER,
         path: "",
         component: () => import("@/views/teacher/Home")
+      },
+      {
+        path: "setting",
+        component: () => import("@/layouts/SettingLayout"),
+        children: [
+          {
+            path: "profile",
+            component: () => import("@/views/setting/Profile")
+          },
+          {
+            path: "password",
+            component: () => import("@/views/setting/Password")
+          }
+        ]
       }
     ]
   },
   {
     path: "/course-teacher/:courseId",
     component: () => import("@/layouts/courseLayouts/TeacherCourseLayout.vue"),
-    meta: { requiresAuth: true },
+    meta: { requiresAuth: false },
     children: [
       {
         name: TEACHER_COURSE,
         path: "dashboard",
         component: () => import("@/views/teacher/Dashboard")
+      },
+      {
+        path: "question",
+        component: () => import("@/views/teacher/Question")
+      },
+      {
+        path: "result",
+        component: () => import("@/views/teacher/Result")
       }
     ]
   },
   {
     path: "/course-student/:courseId",
     component: () => import("@/layouts/courseLayouts/StudentCourseLayout.vue"),
-    meta: { requiresAuth: true },
+    meta: { requiresAuth: false },
     children: [
       {
         name: STUDENT_COURSE,
@@ -83,12 +105,26 @@ export default [
   {
     path: "/student",
     component: () => import("@/layouts/HomeLayout"),
-    meta: { requiresAuth: true },
+    meta: { requiresAuth: false },
     children: [
       {
         name: STUDENT,
         path: "",
         component: () => import("@/views/student/Home")
+      },
+      {
+        path: "setting",
+        component: () => import("@/layouts/SettingLayout"),
+        children: [
+          {
+            path: "profile",
+            component: () => import("@/views/setting/Profile")
+          },
+          {
+            path: "password",
+            component: () => import("@/views/setting/Password")
+          }
+        ]
       }
     ]
   },

+ 120 - 0
src/views/setting/Password.vue

@@ -0,0 +1,120 @@
+<template>
+  <div>
+    <b-notification type="is-danger" :active.sync="isServerError">
+      {{ errorMassage }}
+    </b-notification>
+
+    <b-field
+      label="旧密码"
+      :type="$v.oldPassword.$error ? 'is-danger' : ''"
+      :message="$v.oldPassword.$error ? '请输入旧密码' : ''"
+    >
+      <b-input
+        type="password"
+        v-model="$v.oldPassword.$model"
+        placeholder="在这里输入旧密码"
+        password-reveal
+      >
+      </b-input>
+    </b-field>
+    <b-field
+      label="新密码"
+      :type="$v.password.$error ? 'is-danger' : ''"
+      :message="
+        $v.password.$error
+          ? [
+              !$v.password.complex
+                ? '密码至少为8位,且要包含字母和数字'
+                : undefined,
+              !$v.password.required ? '请输入密码' : undefined
+            ]
+          : ''
+      "
+    >
+      <b-input
+        v-model="$v.password.$model"
+        type="password"
+        placeholder="在此输入新密码"
+        password-reveal
+      ></b-input>
+    </b-field>
+
+    <b-field
+      label="再次输入密码"
+      :type="$v.passwordAgain.$error ? 'is-danger' : ''"
+      :message="$v.passwordAgain.$error ? '密码需一致' : ''"
+    >
+      <b-input
+        type="password"
+        v-model="$v.passwordAgain.$model"
+        placeholder="在这里重复密码"
+        password-reveal
+      >
+      </b-input>
+    </b-field>
+    <b-field>
+      <button
+        :class="{ 'is-loading': submitting }"
+        class="button is-primary"
+        @click="registerAction"
+      >
+        更 新
+      </button>
+    </b-field>
+  </div>
+</template>
+
+<script>
+import { required, sameAs } from "vuelidate/lib/validators";
+import { postUserPassword } from "@/api/user";
+
+export default {
+  data() {
+    return {
+      oldPassword: "",
+      password: "",
+      passwordAgain: "",
+      submitting: false,
+      errorMassage: null,
+      isServerError: false
+    };
+  },
+  validations: {
+    oldPassword: {
+      required
+    },
+    password: {
+      required,
+      complex: password =>
+        /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/.test(password)
+    },
+    passwordAgain: {
+      sameAsPassword: sameAs("password")
+    }
+  },
+  methods: {
+    async registerAction() {
+      this.$v.$touch();
+      if (!this.$v.$invalid) {
+        this.submitting = true;
+        const { password = "", oldPassword = "" } = this;
+        const { id, username, nickname } = this.$store.state.user.profile;
+        try {
+          await postUserPassword(id, { oldPassword, newPassword: password });
+          this.$toast.open({
+            message: `用户「${
+              nickname !== null && nickname.length > 0 ? nickname : username
+            }」密码修改成功`,
+            type: "is-success"
+          });
+        } catch (e) {
+          this.errorMassage = e.message;
+          this.isServerError = true;
+        } finally {
+          this.submitting = false;
+        }
+      }
+    }
+  }
+};
+</script>

+ 88 - 0
src/views/setting/Profile.vue

@@ -0,0 +1,88 @@
+<template>
+  <div>
+    <b-notification type="is-danger" :active.sync="isServerError">
+      {{ errorMassage }}
+    </b-notification>
+    <b-field
+      label="昵称"
+      :type="$v.nickname.$error ? 'is-danger' : ''"
+      :message="$v.nickname.$error ? '昵称须由长度为4至16的字母或数字组成' : ''"
+    >
+      <b-input
+        v-model="$v.nickname.$model"
+        placeholder="在此输入昵称"
+      ></b-input>
+    </b-field>
+    <b-field label="介绍一下你自己">
+      <b-input maxlength="100" v-model="bio" type="textarea"></b-input>
+    </b-field>
+    <b-field>
+      <button
+        :class="{ 'is-loading': submitting }"
+        class="button is-primary"
+        @click="updateProfileAction"
+      >
+        更 新
+      </button>
+    </b-field>
+  </div>
+</template>
+
+<script>
+import { alphaNum, minLength, maxLength } from "vuelidate/lib/validators";
+import { postUserProfile } from "@/api/user";
+import { FETCH_PROFILE } from "@/store/type/actions.type";
+
+export default {
+  data() {
+    return {
+      nickname: "",
+      bio: "",
+      submitting: false,
+      errorMassage: null,
+      isServerError: false
+    };
+  },
+  validations: {
+    nickname: {
+      alphaNum,
+      minLength: minLength(4),
+      maxLength: maxLength(16)
+    }
+  },
+  mounted() {
+    this.getProfileFromStore();
+  },
+  methods: {
+    async updateProfileAction() {
+      this.$v.$touch();
+      if (!this.$v.$invalid) {
+        this.submitting = true;
+        const { id, username } = this.$store.state.user.profile;
+        const { bio = "", nickname = "" } = this;
+        try {
+          await postUserProfile(id, { bio, nickname });
+          await this.$store.dispatch(FETCH_PROFILE, id);
+          this.$toast.open({
+            message: `用户「${
+              nickname.length === 0 ? username : nickname
+            }」修改成功`,
+            type: "is-success"
+          });
+          this.getProfileFromStore();
+        } catch (e) {
+          this.errorMassage = e.message;
+          this.isServerError = true;
+        } finally {
+          this.submitting = false;
+        }
+      }
+    },
+    getProfileFromStore() {
+      const { nickname = "", bio = "" } = this.$store.state.user.profile;
+      this.nickname = nickname;
+      this.bio = bio;
+    }
+  }
+};
+</script>

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

@@ -59,6 +59,7 @@ export default {
     );
   }
 };
+3;
 </script>
 
 <style lang="scss" scoped>

+ 42 - 3
src/views/teacher/Home/index.vue

@@ -2,9 +2,10 @@
   <div>
     <section class="hero">
       <div class="hero-body">
-        <div class="container">
-          <h1 class="title">我的课程</h1>
-          <h2 class="subtitle">展示我开设的全部课程</h2>
+        <div class="container hint">
+          <h1 class="title">我开设的课程</h1>
+          <h2 class="subtitle">请点击下方按钮新建课程</h2>
+          <img class="newSubject" src="../../../assets/white.png" />
         </div>
       </div>
     </section>
@@ -37,3 +38,41 @@ export default {
   }
 };
 </script>
+<style lang="scss">
+@import "../../../assets/scss/app";
+.container.hint {
+  padding: 30px 20px 20px 30px;
+
+  /*background-image: linear-gradient(to right, #6a11cb 0%, #2575fc 100%);*/
+  /*TODO: 寻找质量更高的图片以防止对比度过低 */
+  background-image: url("../../../assets/login_background.png");
+  /*background-size: cover;*/
+  box-shadow: $default-shadow;
+  border-radius: $default-radius;
+  .title {
+    color: #ffffff;
+  }
+  .subtitle {
+    margin-top: -0.8em;
+    font-size: 1rem;
+    font-weight: bold;
+    color: #ffffff;
+  }
+}
+.newSubject {
+  height: 80px;
+  width: 80px;
+  position: absolute;
+  bottom: -40px;
+  left: 20px;
+  transition-property: all;
+  transition-duration: 0.2s;
+
+  &:hover {
+    height: 90px;
+    width: 90px;
+    bottom: -45px;
+    left: 15px;
+  }
+}
+</style>

+ 201 - 0
src/views/teacher/Question/index.vue

@@ -0,0 +1,201 @@
+<template>
+  <div>
+    <nav class="breadcrumb" aria-label="breadcrumbs">
+      <ul>
+        <li><router-link :to="{ name: 'HOME' }">所有课程</router-link></li>
+        <li>
+          <router-link :to="`/course-teacher/${course.id}/dashboard`">{{
+            course.name
+          }}</router-link>
+        </li>
+        <li class="is-active">
+          <router-link :to="`/student-course/${course.id}/question`"
+            >查看题目</router-link
+          >
+        </li>
+      </ul>
+    </nav>
+    <!-- container面板-->
+    <div class="data-table">
+      <!--下拉框&搜索框-->
+      <div class="form-group">
+        <!--下拉框-->
+        <b-field>
+          <b-select placeholder="选择题目类型">
+            <option
+              v-for="option in questionType"
+              :value="option.title"
+              :key="option.id"
+            >
+              {{ option.title }}
+            </option>
+          </b-select>
+        </b-field>
+        <!--搜索框-->
+        <b-field>
+          <b-input
+            placeholder="输入关键字查找题目"
+            type="search"
+            icon="magnify"
+          >
+          </b-input>
+        </b-field>
+      </div>
+      <!--题目table-->
+      <div class="question-list">
+        <b-table
+          :data="data"
+          paginated
+          per-page="5"
+          detailed
+          detail-key="id"
+          @details-open="
+            (row, index) => $toast.open(`Expanded ${row.user.first_name}`)
+          "
+        >
+          <template slot-scope="props">
+            <b-table-column field="id" label="ID" width="40" numeric sortable>
+              <!--{{ props.row.id }}-->1
+            </b-table-column>
+
+            <b-table-column field="user.first_name" label="题目名称">
+              {{ props.row.name }}
+            </b-table-column>
+
+            <b-table-column field="user.last_name" label="题目描述">
+              <div class="description">{{ props.row.description }}</div>
+            </b-table-column>
+
+            <b-table-column
+              field="difficulty"
+              label="题目难度"
+              sortable
+              centered
+            >
+              <span class="tag is-success">
+                {{ props.row.difficulty == 1 ? "简单" : "困难" }}
+              </span>
+            </b-table-column>
+
+            <b-table-column
+              label="平均时长"
+              field="expectTime"
+              sortable
+              centered
+            >
+              {{ props.row.expectTime }}分钟
+            </b-table-column>
+            <b-table-column label="题目标签">
+              {{ props.row.assignmentType.value }}
+            </b-table-column>
+          </template>
+
+          <template slot="detail" slot-scope="props">
+            <article class="media">
+              <!--<figure class="media-left">-->
+              <!--<p class="image is-64x64">-->
+              <!--<img src="/static/img/placeholder-128x128.png">-->
+              <!--</p>-->
+              <!--</figure>-->
+              <div class="media-content">
+                <div class="content">
+                  <p>
+                    <strong>{{ props.row.name }}</strong>
+                    <small>@{{ props.row.expectTime }}分钟</small> <br />
+                    {{ props.row.description }}
+                  </p>
+                </div>
+              </div>
+            </article>
+          </template>
+        </b-table>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import { mapState } from "vuex";
+import { getStudentCourseReviewList } from "@/api/review";
+import { getTeacherQuestions } from "@/api/question";
+
+// const data = [
+//   {
+//     id: 1,
+//     name: "第一题",
+//     description: "这题xxxxxxxx",
+//     difficulty: "easy",
+//     expectTime: 20,
+//     assignmentType: "文档作业"
+//   },
+//   {
+//     id: 2,
+//     name: "第一题",
+//     description: "这题xxxxxxxx",
+//     difficulty: "easy",
+//     expectTime: 25,
+//     assignmentType: "文档作业"
+//   },
+//   {
+//     id: 3,
+//     name: "第一题",
+//     description: "这题xxxxxxxx",
+//     difficulty: "easy",
+//     expectTime: 35,
+//     assignmentType: "文档作业"
+//   }
+// ];
+
+export default {
+  components: {},
+  computed: {
+    ...mapState({
+      course: state => state.course.currentCourse
+    })
+  },
+  data() {
+    return {
+      courses: [],
+      review: [],
+      questionType: [
+        { id: 1, title: "文档作业题目" },
+        { id: 2, title: "代码作业题目" }
+      ],
+      data: []
+    };
+  },
+  mounted() {
+    getStudentCourseReviewList(this.courses.id).then(
+      value => (this.review = value.data)
+    );
+    getTeacherQuestions().then(value => (this.data = value.data));
+  }
+};
+</script>
+<style lang="scss" scoped>
+.breadcrumb a {
+  /*color: #7957d5;*/
+  font-weight: bold;
+}
+.data-table {
+  width: 100%;
+  padding: 30px;
+  min-height: 80vh;
+  background-color: #ffffff;
+}
+.question-list {
+  margin-top: 30px;
+}
+.form-group {
+  display: flex;
+  justify-content: space-between;
+}
+.description {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+  word-wrap: break-word;
+  word-break: break-all;
+  display: -webkit-box;
+}
+</style>

+ 356 - 0
src/views/teacher/Result/index.vue

@@ -0,0 +1,356 @@
+<template>
+  <div>
+    <nav class="breadcrumb" aria-label="breadcrumbs">
+      <ul>
+        <li><router-link :to="{ name: 'HOME' }">所有课程</router-link></li>
+        <li>
+          <router-link :to="`/course-teacher/${course.id}/dashboard`">{{
+            course.name
+          }}</router-link>
+        </li>
+        <li class="is-active">
+          <router-link :to="`/student-course/${course.id}/result`"
+            >查看成绩</router-link
+          >
+        </li>
+      </ul>
+    </nav>
+    <!-- container面板-->
+    <div class="data-table">
+      <!--下拉框&搜索框-->
+      <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
+          >
+        </b-field>
+      </div>
+      <!--题目table-->
+      <div class="question-list">
+        <b-table :data="data" paginated per-page="5" detailed detail-key="id">
+          <template slot-scope="props">
+            <b-table-column field="id" label="ID" width="40" numeric sortable>
+              {{ props.row.id }}
+            </b-table-column>
+
+            <b-table-column label="文档作业" centered>
+              <div
+                class="result"
+                v-for="item in props.row.documentHomeworkResult"
+                :key="item.id"
+              >
+                {{ item.name }} : {{ item.result }}
+              </div>
+            </b-table-column>
+
+            <b-table-column label="代码作业" centered>
+              <div
+                class="result"
+                v-for="item in props.row.codeHomeworkResult"
+                :key="item.id"
+              >
+                {{ item.name }} : {{ item.result }}
+              </div>
+            </b-table-column>
+
+            <b-table-column label="互评作业" centered>
+              <div
+                class="result"
+                v-for="item in props.row.reviewHomeworkResult"
+                :key="item.id"
+              >
+                {{ item.name }} : {{ item.result }}
+              </div>
+            </b-table-column>
+            <b-table-column field="difficulty" label="总评" sortable centered>
+              <span
+                :class="[
+                  'tag',
+                  props.row.finalResult > 60 ? 'is-success' : 'is-danger'
+                ]"
+              >
+                {{ props.row.finalResult }}
+              </span>
+            </b-table-column>
+          </template>
+
+          <template slot="detail">
+            <article class="media">
+              <!--<figure class="media-left">-->
+              <!--<p class="image is-64x64">-->
+              <!--<img src="/static/img/placeholder-128x128.png">-->
+              <!--</p>-->
+              <!--</figure>-->
+              <div class="media-content">
+                <div class="content">
+                  <p>
+                    <strong>成绩详情</strong>
+                    <small>这里可以放具体的作业成绩,前面可以放总评啥的</small>
+                    <br />
+                  </p>
+                </div>
+              </div>
+            </article>
+          </template>
+        </b-table>
+      </div>
+    </div>
+    <!-- 成绩比例调控模态框 -->
+    <b-modal :active.sync="isCardModalActive" :width="320" scroll="keep">
+      <div class="card">
+        <div class="title">
+          成绩比例调控<small>&nbsp;&nbsp;请按百分比填写</small>
+        </div>
+        <div class="content">
+          <b-field label="文档作业比例">
+            <b-input v-model="name"></b-input>
+          </b-field>
+          <b-field label="互评作业比例">
+            <b-input v-model="name"></b-input>
+          </b-field>
+          <b-field label="代码作业比例">
+            <b-input v-model="name"></b-input>
+          </b-field>
+          <a class="button is-link" @click="isCardModalActive = false"
+            >确认调整</a
+          >
+        </div>
+      </div>
+    </b-modal>
+  </div>
+</template>
+<script>
+import { mapState } from "vuex";
+import { getStudentCourseReviewList } from "@/api/review";
+const data = [
+  {
+    id: 1,
+    documentHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      },
+      {
+        id: 2,
+        name: "第二次作业",
+        result: 85
+      },
+      {
+        id: 3,
+        name: "第三次作业",
+        result: 99
+      }
+    ],
+    reviewHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      }
+    ],
+    codeHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      },
+      {
+        id: 2,
+        name: "第二次作业",
+        result: 85
+      }
+    ],
+    finalResult: 99
+  },
+  {
+    id: 2,
+    documentHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      },
+      {
+        id: 2,
+        name: "第二次作业",
+        result: 85
+      },
+      {
+        id: 3,
+        name: "第三次作业",
+        result: 99
+      }
+    ],
+    reviewHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      }
+    ],
+    codeHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      },
+      {
+        id: 2,
+        name: "第二次作业",
+        result: 85
+      }
+    ],
+    finalResult: 99
+  },
+  {
+    id: 3,
+    documentHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      },
+      {
+        id: 2,
+        name: "第二次作业",
+        result: 85
+      },
+      {
+        id: 3,
+        name: "第三次作业",
+        result: 99
+      }
+    ],
+    reviewHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      }
+    ],
+    codeHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      },
+      {
+        id: 2,
+        name: "第二次作业",
+        result: 85
+      }
+    ],
+    finalResult: 99
+  },
+  {
+    id: 4,
+    documentHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      },
+      {
+        id: 2,
+        name: "第二次作业",
+        result: 85
+      },
+      {
+        id: 3,
+        name: "第三次作业",
+        result: 99
+      }
+    ],
+    reviewHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      }
+    ],
+    codeHomeworkResult: [
+      {
+        id: 1,
+        name: "第一次作业",
+        result: 80
+      },
+      {
+        id: 2,
+        name: "第二次作业",
+        result: 85
+      }
+    ],
+    finalResult: 99
+  }
+];
+
+export default {
+  components: {},
+  computed: {
+    ...mapState({
+      course: state => state.course.currentCourse
+    })
+  },
+  data() {
+    return {
+      courses: [],
+      review: [],
+      questionType: [
+        { id: 1, title: "文档作业题目" },
+        { id: 2, title: "代码作业题目" }
+      ],
+      data,
+      isCardModalActive: false
+    };
+  },
+  mounted() {
+    getStudentCourseReviewList(this.courses.id).then(
+      value => (this.review = value.data)
+    );
+  }
+};
+</script>
+<style lang="scss" scoped>
+.breadcrumb a {
+  /*color: #7957d5;*/
+  font-weight: bold;
+}
+.data-table {
+  width: 100%;
+  padding: 30px;
+  min-height: 80vh;
+  background-color: #ffffff;
+}
+.question-list {
+  margin-top: 30px;
+}
+.form-group {
+  display: flex;
+  justify-content: space-between;
+}
+.result {
+  font-size: 0.8rem;
+}
+.card {
+  padding: 30px;
+  .title {
+    font-size: 1.25rem;
+    small {
+      color: gray;
+      font-size: 1rem;
+    }
+  }
+  .content {
+    a {
+      margin-top: 30px;
+      width: 100%;
+    }
+  }
+}
+.button {
+  background-color: #6172ec;
+}
+</style>