소스 검색

Merge branch 'feature/basic-layout-bay' of SEEC-II-FrontEnd-Team/SEEC-II-FrontEnd into develop

WuXinyu 7 년 전
부모
커밋
59194ce338
43개의 변경된 파일1045개의 추가작업 그리고 347개의 파일을 삭제
  1. 3 3
      mock/modules/auth/index.js
  2. 1 1
      mock/routes.js
  3. 1 1
      mock/serializers/user/UserSerializer.js
  4. 1 22
      src/App.vue
  5. 4 4
      src/api/auth.js
  6. 11 19
      src/assets/scss/app.scss
  7. 64 0
      src/components/Card/index.vue
  8. 2 0
      src/data/userType.js
  9. 0 8
      src/layouts/ExampleLayout.vue
  10. 87 0
      src/layouts/HomeLayout/HomeHeader/index.vue
  11. 30 0
      src/layouts/HomeLayout/index.vue
  12. 1 1
      src/layouts/LoginLayout.vue
  13. 104 0
      src/layouts/courseLayouts/LayoutStructure.vue
  14. 15 0
      src/layouts/courseLayouts/StudentCourseLayout.vue
  15. 48 0
      src/layouts/courseLayouts/StudentMenu.vue
  16. 15 0
      src/layouts/courseLayouts/TeacherCourseLayout.vue
  17. 40 0
      src/layouts/courseLayouts/TeacherMenu.vue
  18. 20 38
      src/router/index.js
  19. 7 4
      src/router/name.js
  20. 107 0
      src/router/routes.js
  21. 0 62
      src/store/auth.module.js
  22. 3 3
      src/store/index.js
  23. 26 0
      src/store/modules/course.module.js
  24. 28 0
      src/store/modules/user.module.js
  25. 1 6
      src/store/type/actions.type.js
  26. 1 7
      src/store/type/mutations.type.js
  27. 0 83
      src/store/user.module.js
  28. 12 0
      src/util/auth.js
  29. 2 2
      src/util/profileStorage.js
  30. 24 11
      src/util/request.js
  31. 0 12
      src/util/tokenStorage.js
  32. 0 3
      src/views/About.vue
  33. 0 15
      src/views/Home.vue
  34. 51 42
      src/views/login/Login.vue
  35. 175 0
      src/views/login/Register.vue
  36. 3 0
      src/views/student/Code/index.vue
  37. 3 0
      src/views/student/Dashboard/index.vue
  38. 3 0
      src/views/student/Document/index.vue
  39. 3 0
      src/views/student/Group/index.vue
  40. 39 0
      src/views/student/Home/index.vue
  41. 68 0
      src/views/student/Review/index.vue
  42. 3 0
      src/views/teacher/Dashboard/index.vue
  43. 39 0
      src/views/teacher/Home/index.vue

+ 3 - 3
mock/modules/auth/index.js

@@ -19,11 +19,11 @@ router.route("/login").post((req, res) => {
     case "teacher":
       setSendValue("TEACHER");
       break;
-    case "error":
+    case "error@123.com":
       res.status(418).send({ message: "用户名密码不匹配" });
       break;
     default:
-      setSendValue("STUDENT");
+      setSendValue();
       break;
   }
 });
@@ -31,7 +31,7 @@ router.route("/login").post((req, res) => {
 router.route("/register").post((req, res) => {
   //注册
   res.send({ data: userSerializer() });
-  // res.status(418).send({ message: '用户名已存在' });
+  // res.status(400).send({ message: "用户名已存在" });
 });
 
 module.exports = router;

+ 1 - 1
mock/routes.js

@@ -1,7 +1,7 @@
 const express = require("express");
 const router = express.Router();
 
-router.use("/login", require("./modules/auth"));
+router.use("/auth", require("./modules/auth"));
 router.use("/user", require("./modules/user"));
 router.use("/course", require("./modules/course"));
 router.use("/group", require("./modules/group"));

+ 1 - 1
mock/serializers/user/UserSerializer.js

@@ -14,7 +14,7 @@ const makeId = require("../../util/makeId");
  * @param role
  * @returns {UserSerializerType}
  */
-module.exports = (role = "STUDENT") => {
+module.exports = (role = "TEACHER") => {
   return {
     id: makeId(),
     role: role,

+ 1 - 22
src/App.vue

@@ -1,30 +1,9 @@
 <template>
-  <div id="app">
-    <!--<div id="nav">-->
-    <!--<router-link to="/">Home</router-link> |-->
-    <!--<router-link to="/about">About</router-link>-->
-    <!--</div>-->
-    <router-view />
-  </div>
+  <div id="app"><router-view /></div>
 </template>
 
 <style lang="scss">
 #app {
-  /*font-family: "Avenir", Helvetica, Arial, sans-serif;*/
-  /*-webkit-font-smoothing: antialiased;*/
-  /*-moz-osx-font-smoothing: grayscale;*/
-  /*text-align: center;*/
-  /*color: #2c3e50;*/
   background: #f4f7fb;
 }
-/*#nav {*/
-/*padding: 30px;*/
-/*a {*/
-/*font-weight: bold;*/
-/*color: #2c3e50;*/
-/*&.router-link-exact-active {*/
-/*color: #42b983;*/
-/*}*/
-/*}*/
-/*}*/
 </style>

+ 4 - 4
src/api/auth.js

@@ -19,18 +19,18 @@ export const login = ({ username, password }) => {
 
 /**
  * 注册 register
- * @param {string} nickname
+ * @param {string|null} nickname
  * @param {string} username
  * @param {string} password
  * @returns {Promise<{data:UserSerializerType}>}
  */
-export const register = ({ nickname, username, password }) => {
+export const register = ({ nickname = null, username, password }) => {
   return request(`${AUTH_MODULE}/register`, {
     method: "POST",
     body: {
       nickname: nickname || null,
-      username: username || null,
-      password: password || null
+      username,
+      password
     }
   });
 };

+ 11 - 19
src/assets/scss/app.scss

@@ -1,28 +1,20 @@
 @import "~bulma/sass/utilities/initial-variables";
 @import "~bulma/sass/utilities/functions";
-// 1. Set your own initial variables and derived 
-//    variables in _variables.scss
 @import "variables";
 
-// 2. Setup your Custom Colors
-$linkedin: #0077b5;
-$linkedin-invert: findColorInvert($linkedin);
-$twitter: #55acee;
-$twitter-invert: findColorInvert($twitter);
-$github: #333;
-$github-invert: findColorInvert($github);
-
 @import "~bulma/sass/utilities/derived-variables";
 
-// 3. Add new color variables to the color map.
-$addColors: (
-  "twitter":($twitter, $twitter-invert),
-  "linkedin": ($linkedin, $linkedin-invert),
-  "github": ($github, $github-invert)
-);
-$colors: map-merge($colors, $addColors);
-
 @import "~bulma";
 @import "~buefy/src/scss/buefy";
 
-// 4. Provide custom buefy overrides and site styles here
+
+$border-color:#b9c7d8;
+$default-border:1px solid #c9d8ea;
+
+.has-text-primary{
+  color:#209cee !important;
+}
+
+.input{
+  box-shadow: none !important;
+}

+ 64 - 0
src/components/Card/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <div
+    class="custom-card"
+    :class="{ hoverable: hoverable }"
+    :style="{ padding: hasPadding ? '20px' : 0 }"
+  >
+    <slot></slot>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    hoverable: {
+      type: Boolean,
+      default: false
+    },
+    hasPadding: {
+      type: Boolean,
+      default: true
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../assets/scss/app";
+
+.custom-card {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  background: #ffffff;
+  border: $default-border;
+  color: #152935;
+}
+
+.hoverable {
+  cursor: pointer;
+  position: relative;
+  text-decoration: none;
+  z-index: 1;
+}
+.hoverable:after {
+  pointer-events: none;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  content: "";
+  box-sizing: content-box;
+  top: -5px;
+  left: -5px;
+  padding: 5px;
+  box-shadow: 0 0 0 4px #8897a2;
+  transition: transform 0.2s, opacity 0.2s;
+  transform: scaleY(0.9) scaleX(0.95);
+  opacity: 0;
+}
+
+.hoverable:hover:after {
+  transform: scale(1);
+  opacity: 1;
+}
+</style>

+ 2 - 0
src/data/userType.js

@@ -0,0 +1,2 @@
+export const TEACHER = "TEACHER";
+export const STUDENT = "STUDENT";

+ 0 - 8
src/layouts/ExampleLayout.vue

@@ -1,8 +0,0 @@
-<template>
-  <div>
-    <div></div>
-    <router-view></router-view>
-  </div>
-</template>
-
-<style lang="scss"></style>

+ 87 - 0
src/layouts/HomeLayout/HomeHeader/index.vue

@@ -0,0 +1,87 @@
+<template>
+  <nav class="navbar nav-section">
+    <div class="container">
+      <div class="navbar-brand">
+        <a class="navbar-item header-logo"> MOOC </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>
+    </div>
+  </nav>
+</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;
+    }
+  },
+  async mounted() {
+    await this.$store.dispatch(FETCH_PROFILE, this.profile.id);
+  }
+};
+</script>
+
+<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;
+  padding-right: 20px;
+  a {
+    color: #3d70b2;
+  }
+}
+.nav-active {
+  background-color: #fafafa;
+}
+
+@media screen {
+}
+</style>

+ 30 - 0
src/layouts/HomeLayout/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <section class="main-body">
+    <home-header />
+    <router-view class="body-section" />
+    <footer class="footer" style="background: transparent">
+      <div class="content has-text-centered">@nju software</div>
+    </footer>
+  </section>
+</template>
+
+<script>
+import HomeHeader from "@/layouts/HomeLayout/HomeHeader";
+export default {
+  components: {
+    HomeHeader
+  }
+};
+</script>
+
+<style lang="scss">
+/*@import "bulma/sass/utilities/_all.sass";*/
+.main-body {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  .body-section {
+    flex: 1;
+  }
+}
+</style>

+ 1 - 1
src/layouts/LoginLayout.vue

@@ -15,7 +15,7 @@
 <style lang="scss" scoped>
 .logo {
   text-align: center;
-  padding: 20vh 0 0;
+  padding: 10vh 0 0;
   font-size: 40px;
   /*<!--color: $logo-color;-->*/
   /*<!--font-size: $logo-font-size;-->*/

+ 104 - 0
src/layouts/courseLayouts/LayoutStructure.vue

@@ -0,0 +1,104 @@
+<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>
+        <slot />
+      </div>
+    </div>
+    <div class="router-section"><router-view /></div>
+  </div>
+</template>
+
+<script>
+import { FETCH_CURRENT_COURSE } from "@/store/type/actions.type";
+import { mapState } from "vuex";
+
+export default {
+  computed: {
+    ...mapState({
+      course: state => state.course.currentCourse
+    })
+  },
+  data() {
+    return {
+      isLoading: false
+    };
+  },
+  async mounted() {
+    const { courseId } = this.$route.params;
+    this.isLoading = true;
+    await this.$store.dispatch(FETCH_CURRENT_COURSE, courseId);
+    this.isLoading = false;
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../assets/scss/app.scss";
+
+$menu-width: 300px;
+$menu-padding: 24px;
+$screen-width: 1450px;
+$left-padding: "(100% -" #{$screen-width} "-" #{$menu-padding} "* 2) / 2";
+$left-width: #{$left-padding} + #{$menu-width};
+$left-padding-calc: calc(#{$left-padding});
+$left-width-calc: calc(#{$left-width});
+$top-section-height: 4.25rem;
+
+.course-layout {
+  display: flex;
+  min-height: 100vh;
+  justify-content: stretch;
+  position: relative;
+}
+
+.router-section {
+  padding: 24px;
+  flex-grow: 1;
+  max-width: 1000px;
+}
+.menu-section {
+  min-width: $menu-width;
+  width: $left-width-calc;
+  background: white;
+  border-right: 1px solid #c9d8ea;
+  padding-left: $left-padding-calc;
+  .menu-sticky {
+    width: $menu-width;
+    position: sticky;
+    overflow-y: auto;
+    padding: 0 $menu-padding;
+    top: 0;
+    height: 100vh;
+
+    .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;
+      padding-right: 20px;
+      line-height: $top-section-height;
+      a {
+        color: #3d70b2;
+      }
+    }
+    .course-detail {
+      position: relative;
+      padding: 15px;
+      background: #f4f7fb;
+      border: 1px solid #c9d8ea;
+      border-radius: 3px;
+      box-shadow: 0 1px 2px rgba(201, 216, 234, 0.5);
+      margin-bottom: 1em;
+      min-height: 54px;
+    }
+  }
+}
+</style>

+ 15 - 0
src/layouts/courseLayouts/StudentCourseLayout.vue

@@ -0,0 +1,15 @@
+<template>
+  <layout-structure> <student-menu /></layout-structure>
+</template>
+
+<script>
+import StudentMenu from "./StudentMenu";
+import LayoutStructure from "./LayoutStructure";
+
+export default {
+  components: {
+    StudentMenu,
+    LayoutStructure
+  }
+};
+</script>

+ 48 - 0
src/layouts/courseLayouts/StudentMenu.vue

@@ -0,0 +1,48 @@
+<template>
+  <aside class="menu">
+    <p class="menu-label">总览</p>
+    <ul class="menu-list">
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/dashboard`">
+          工作台
+        </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>
+      </li>
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/document`"
+          >文档作业</router-link
+        >
+      </li>
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/review`"
+          >评审作业</router-link
+        >
+      </li>
+    </ul>
+    <p class="menu-label">小组</p>
+    <ul class="menu-list">
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/group`"
+          >我的小组</router-link
+        >
+      </li>
+    </ul>
+  </aside>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      prefix: `/course-student/${this.$route.params.courseId}`
+    };
+  }
+};
+</script>

+ 15 - 0
src/layouts/courseLayouts/TeacherCourseLayout.vue

@@ -0,0 +1,15 @@
+<template>
+  <layout-structure> <teacher-menu /></layout-structure>
+</template>
+
+<script>
+import TeacherMenu from "./TeacherMenu";
+import LayoutStructure from "./LayoutStructure";
+
+export default {
+  components: {
+    TeacherMenu,
+    LayoutStructure
+  }
+};
+</script>

+ 40 - 0
src/layouts/courseLayouts/TeacherMenu.vue

@@ -0,0 +1,40 @@
+<template>
+  <aside class="menu">
+    <p class="menu-label">总览</p>
+    <ul class="menu-list">
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}`"
+          >工作台</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
+        >
+      </li>
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/document`"
+          >文档作业</router-link
+        >
+      </li>
+      <li>
+        <router-link active-class="is-active" :to="`${prefix}/review`"
+          >评审作业</router-link
+        >
+      </li>
+    </ul>
+  </aside>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      prefix: `/course-teacher/${this.$route.params.courseId}`
+    };
+  }
+};
+</script>

+ 20 - 38
src/router/index.js

@@ -1,50 +1,32 @@
 import Vue from "vue";
 import Router from "vue-router";
-import {
-  LOGIN_ROUTER,
-  // REGISTER_ROUTER,
-  HOME_ROUTER
-  // MIXER_ROUTER
-} from "@/router/name";
+import routes from "./routes";
+import { judgeLogin } from "@/util/auth";
+import { LOGIN } from "@/router/name";
 
 Vue.use(Router);
 
 const router = new Router({
   mode: "history",
   base: process.env.BASE_URL,
-  routes: [
-    {
-      path: "/login",
-      component: () => import("@/layouts/LoginLayout"),
-      meta: { requiresAuth: false },
-      children: [
-        // {
-        //   name: REGISTER_ROUTER,
-        //   path: "/register",
-        //   component: () => import("@/views/Register"),
-        //   meta: { requiresAuth: false }
-        // },
-        {
-          name: LOGIN_ROUTER,
-          path: "",
-          component: () => import("@/views/login/Login.vue"),
-          meta: { requiresAuth: false }
-        }
-      ]
-    },
-    {
-      name: HOME_ROUTER,
-      path: "/",
-      component: () => import("@/views/Home"),
-      meta: { requiresAuth: true }
+  routes
+});
+
+router.beforeEach((to, from, next) => {
+  if (to.matched.some(record => record.meta.requiresAuth)) {
+    // this route requires auth, check if logged in
+    // if not, redirect to login page.
+    if (!judgeLogin()) {
+      next({
+        name: LOGIN,
+        query: { redirect: to.fullPath }
+      });
+    } else {
+      next();
     }
-    // {
-    //   name: MIXER_ROUTER,
-    //   path: "/mixer",
-    //   component: () => import("@/views/Mixer"),
-    //   meta: { requiresAuth: true }
-    // }
-  ]
+  } else {
+    next();
+  }
 });
 
 export default router;

+ 7 - 4
src/router/name.js

@@ -1,4 +1,7 @@
-export const LOGIN_ROUTER = "login";
-export const REGISTER_ROUTER = "register";
-export const HOME_ROUTER = "home";
-export const MIXER_ROUTER = "mixer";
+export const LOGIN = "LOGIN";
+export const REGISTER = "REGISTER";
+export const HOME = "HOME";
+export const TEACHER = "TEACHER";
+export const STUDENT = "STUDENT";
+export const TEACHER_COURSE = "TEACHER_COURSE";
+export const STUDENT_COURSE = "STUDENT_COURSE";

+ 107 - 0
src/router/routes.js

@@ -0,0 +1,107 @@
+import {
+  HOME,
+  LOGIN,
+  REGISTER,
+  STUDENT,
+  STUDENT_COURSE,
+  TEACHER,
+  TEACHER_COURSE
+} from "@/router/name";
+import { judgeLogin } from "@/util/auth";
+import { getUserProfile } from "@/util/profileStorage";
+
+export default [
+  {
+    path: "/login",
+    component: () => import("@/layouts/LoginLayout"),
+    meta: { requiresAuth: false },
+    children: [
+      {
+        name: REGISTER,
+        path: "/register",
+        component: () => import("@/views/login/Register.vue")
+      },
+      {
+        name: LOGIN,
+        path: "",
+        component: () => import("@/views/login/Login.vue")
+      }
+    ]
+  },
+  {
+    path: "/teacher",
+    component: () => import("@/layouts/HomeLayout"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: TEACHER,
+        path: "",
+        component: () => import("@/views/teacher/Home")
+      }
+    ]
+  },
+  {
+    path: "/course-teacher/:courseId",
+    component: () => import("@/layouts/courseLayouts/StudentCourseLayout.vue"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: TEACHER_COURSE,
+        path: "",
+        component: () => import("@/views/teacher/Dashboard")
+      }
+    ]
+  },
+  {
+    path: "/course-student/:courseId",
+    component: () => import("@/layouts/courseLayouts/StudentCourseLayout.vue"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: STUDENT_COURSE,
+        path: "dashboard",
+        component: () => import("@/views/student/Dashboard")
+      },
+      {
+        path: "code",
+        component: () => import("@/views/student/Code")
+      },
+      {
+        path: "document",
+        component: () => import("@/views/student/Document")
+      },
+      {
+        path: "review",
+        component: () => import("@/views/student/Review")
+      },
+      {
+        path: "code",
+        component: () => import("@/views/student/Code")
+      }
+    ]
+  },
+  {
+    path: "/student",
+    component: () => import("@/layouts/HomeLayout"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: STUDENT,
+        path: "",
+        component: () => import("@/views/student/Home")
+      }
+    ]
+  },
+  {
+    path: "/",
+    name: HOME,
+    redirect: () => {
+      if (judgeLogin()) {
+        const { role } = getUserProfile();
+        return { name: role };
+      } else {
+        return { name: LOGIN };
+      }
+    }
+  }
+];

+ 0 - 62
src/store/auth.module.js

@@ -1,62 +0,0 @@
-import { login, register } from "@/api/auth";
-import { LOGIN, LOGOUT, REGISTER } from "@/store/type/actions.type";
-import {
-  SET_AUTH,
-  SET_LOGIN_ERROR,
-  REMOVE_AUTH,
-  SET_REGISTER_ERROR,
-  SET_PROFILE
-} from "@/store/type/mutations.type";
-import { getToken, destroyToken, saveToken } from "@/util/tokenStorage";
-
-const state = {
-  isAuthenticated: !!getToken(),
-  isLoginError: false,
-  isRegisterError: false
-};
-
-const actions = {
-  async [LOGIN](context, credentials) {
-    try {
-      const { token, user } = await login(credentials);
-      context.commit(SET_PROFILE, user);
-      context.commit(SET_AUTH, { token, profile: user });
-      context.commit(SET_LOGIN_ERROR, false);
-    } catch (e) {
-      context.commit(SET_LOGIN_ERROR, true);
-    }
-  },
-  [LOGOUT](context) {
-    context.commit(REMOVE_AUTH);
-  },
-  async [REGISTER](context, credentials) {
-    try {
-      await register(credentials);
-    } catch (e) {
-      context.commit(SET_REGISTER_ERROR, true);
-    }
-  }
-};
-
-const mutations = {
-  [SET_LOGIN_ERROR](state, error) {
-    state.isLoginError = error;
-  },
-  [SET_REGISTER_ERROR](state, error) {
-    state.isRegisterError = error;
-  },
-  [SET_AUTH](state, { token, profile }) {
-    state.isAuthenticated = true;
-    saveToken(token, profile);
-  },
-  [REMOVE_AUTH](state) {
-    state.isAuthenticated = false;
-    destroyToken();
-  }
-};
-
-export default {
-  state,
-  actions,
-  mutations
-};

+ 3 - 3
src/store/index.js

@@ -1,14 +1,14 @@
 import Vue from "vue";
 import Vuex from "vuex";
 
-import auth from "./auth.module";
-import user from "./user.module";
+import user from "./modules/user.module";
+import course from "./modules/course.module";
 
 Vue.use(Vuex);
 
 export default new Vuex.Store({
   modules: {
     user,
-    auth
+    course
   }
 });

+ 26 - 0
src/store/modules/course.module.js

@@ -0,0 +1,26 @@
+import * as ACTIONS from "@/store/type/actions.type";
+import * as MUTATIONS from "@/store/type/mutations.type.js";
+import { getCourseDetail } from "@/api/course";
+
+const courseState = {
+  currentCourse: {}
+};
+
+const actions = {
+  async [ACTIONS.FETCH_CURRENT_COURSE](context, courseId) {
+    const { data } = await getCourseDetail(courseId);
+    context.commit(MUTATIONS.SET_CURRENT_COURSE, data);
+  }
+};
+
+const mutations = {
+  [MUTATIONS.SET_CURRENT_COURSE](state, courseSerializer = {}) {
+    state.currentCourse = courseSerializer;
+  }
+};
+
+export default {
+  state: courseState,
+  actions,
+  mutations
+};

+ 28 - 0
src/store/modules/user.module.js

@@ -0,0 +1,28 @@
+import * as ACTIONS from "@/store/type/actions.type";
+import * as MUTATIONS from "@/store/type/mutations.type.js";
+import { getUserProfile, saveProfile } from "@/util/profileStorage";
+import { fetchUserProfile } from "@/api/user";
+
+const userState = {
+  profile: getUserProfile() || {}
+};
+
+const actions = {
+  async [ACTIONS.FETCH_PROFILE](context, userId) {
+    const { data } = await fetchUserProfile(userId);
+    context.commit(MUTATIONS.SET_PROFILE, data);
+  }
+};
+
+const mutations = {
+  [MUTATIONS.SET_PROFILE](state, userSerializer = {}) {
+    state.profile = userSerializer;
+    saveProfile(userSerializer);
+  }
+};
+
+export default {
+  state: userState,
+  actions,
+  mutations
+};

+ 1 - 6
src/store/type/actions.type.js

@@ -1,7 +1,2 @@
-export const LOGIN = "login";
-export const REGISTER = "register";
 export const FETCH_PROFILE = "fetchProfile";
-export const FETCH_PHOTOS = "fetchPhotos";
-export const POST_PHOTOS = "postPhotos";
-export const LOGOUT = "logout";
-export const ORDER_BY = "orderBy";
+export const FETCH_CURRENT_COURSE = "fetchCurrentCourse";

+ 1 - 7
src/store/type/mutations.type.js

@@ -1,8 +1,2 @@
-export const SET_LOGIN_ERROR = "setLoginError";
-export const SET_REGISTER_ERROR = "setRegisterError";
-export const SET_AUTH = "setAuthorization";
 export const SET_PROFILE = "setProfile";
-export const REMOVE_AUTH = "removeAuthorization";
-export const SET_PHOTOS = "setPhotos";
-export const SET_ORDER = "setOrder";
-export const SET_LAST_SEARCH = "setLastSearch";
+export const SET_CURRENT_COURSE = "setCurrentCourse";

+ 0 - 83
src/store/user.module.js

@@ -1,83 +0,0 @@
-import { fetchUserAuthProfile } from "@/api/user";
-import {
-  FETCH_PROFILE,
-  FETCH_PHOTOS,
-  POST_PHOTOS,
-  ORDER_BY
-} from "@/store/type/actions.type";
-import {
-  SET_PROFILE,
-  SET_PHOTOS,
-  SET_ORDER,
-  SET_LAST_SEARCH
-} from "@/store/type/mutations.type";
-import { fetchUserPhotos, postUserPhotos } from "@/api/photo";
-
-const LATEST = "LATEST";
-
-const state = {
-  profile: {
-    id: undefined,
-    username: undefined,
-    nickname: undefined
-  },
-  photos: [],
-  orderType: LATEST,
-  searchBy: -1,
-  lastSearch: -1
-};
-
-const actions = {
-  async [FETCH_PROFILE](context) {
-    if (context.state.profile) {
-      const newProfile = await fetchUserAuthProfile(context.state.profile.id);
-      context.commit(SET_PROFILE, newProfile);
-    }
-  },
-  async [FETCH_PHOTOS](context, tag = -1) {
-    let searchTag = tag;
-    if (tag === "") {
-      searchTag = -1;
-    }
-    const photos = await fetchUserPhotos({
-      tag: searchTag,
-      sort: context.state.orderType
-    });
-    context.commit(SET_PHOTOS, photos);
-    context.commit(SET_LAST_SEARCH, tag);
-  },
-  async [POST_PHOTOS](context, { files, onload }) {
-    if (context.state.profile) {
-      postUserPhotos(context.state.profile.id, files, onload);
-    }
-  },
-  async [ORDER_BY](context, type) {
-    context.commit(SET_ORDER, type);
-    const photos = await fetchUserPhotos({
-      tag: context.state.lastSearch,
-      sort: type
-    });
-    context.commit(SET_PHOTOS, photos);
-  }
-};
-
-const mutations = {
-  [SET_PROFILE](state, profile) {
-    state.profile = profile;
-  },
-  [SET_PHOTOS](state, photos = []) {
-    state.photos = photos;
-  },
-  [SET_ORDER](state, type) {
-    state.orderType = type;
-  },
-  [SET_LAST_SEARCH](state, search) {
-    state.lastSearch = search;
-  }
-};
-
-export default {
-  state,
-  actions,
-  mutations
-};

+ 12 - 0
src/util/auth.js

@@ -0,0 +1,12 @@
+import { getToken } from "@/util/tokenStorage";
+import { getUserProfile } from "@/util/profileStorage";
+
+/**
+ * 判断是否登陆
+ * @return {boolean}
+ */
+export const judgeLogin = () => {
+  const token = getToken();
+  const profile = getUserProfile();
+  return token && profile && typeof profile === "object" && profile.role;
+};

+ 2 - 2
src/util/profileStorage.js

@@ -9,7 +9,7 @@ export const saveProfile = profile =>
 
 /**
  * 获得用户信息
- * @return {UserSerializerType} 用户信息
+ * @return {UserSerializerType|null} 用户信息
  * @exception {Error} profile not found
  */
 export const getUserProfile = () => {
@@ -18,7 +18,7 @@ export const getUserProfile = () => {
   try {
     profile = JSON.parse(profileText);
   } catch (e) {
-    throw new Error("profile not found");
+    return null;
   }
   return profile;
 };

+ 24 - 11
src/util/request.js

@@ -1,14 +1,14 @@
 // ant-design-pro request.js file with MIT license
 import "whatwg-fetch";
 import router from "@/router";
-import { LOGIN_ROUTER } from "@/router/name";
+import { LOGIN } from "@/router/name";
 
 /**
  * @description error
  * @typedef {Object} StatusException
  * @property {string} name status code
  * @property {string} message 消息
- * @property {{code:string,message:string}} response 返回值
+ * @property {{code:string,message:string,body:{code:number,message:string}}} response 返回值
  */
 
 /**
@@ -17,15 +17,23 @@ import { LOGIN_ROUTER } from "@/router/name";
  * @returns {*}
  * @throws StatusException
  */
-function checkStatus(response) {
+const checkStatus = async response => {
   if (response.status >= 200 && response.status < 300) {
     return response;
   }
-  const error = new Error(response.message);
+  let responseBody = {};
+  try {
+    responseBody = await response.json();
+  } catch (e) {
+    console.log(e);
+  }
+  console.log(responseBody);
+  const error = new Error();
   error.name = response.status;
-  error.response = response;
+  error.message = responseBody.message;
+  error.body = responseBody;
   throw error;
-}
+};
 
 /**
  * Requests a URL, returning a promise.
@@ -63,18 +71,23 @@ export default async function request(url, options) {
 
   const response = await fetch(url, newOptions);
   try {
-    checkStatus(response);
+    await checkStatus(response);
   } catch (e) {
-    if (e.name !== 418) {
-      router.push({ name: LOGIN_ROUTER });
+    if (e.name === 401) {
+      router.push({ name: LOGIN });
     } else {
-      throw new Error(e);
+      throw e;
     }
   }
 
   if (newOptions.method === "DELETE" || response.status === 204) {
     return response.text();
   }
+  const Authorization = await response.headers.get("Authorization");
+  const body = await response.json();
+  const header = {
+    Authorization
+  };
 
-  return response.json();
+  return { ...body, _header: header };
 }

+ 0 - 12
src/util/tokenStorage.js

@@ -1,5 +1,4 @@
 const ID_TOKEN_KEY = "id_token";
-const USER_PROFILE = "user_profile";
 
 /**
  * 获得token
@@ -18,14 +17,3 @@ export const saveToken = token =>
   token && window.localStorage.setItem(ID_TOKEN_KEY, token);
 
 export const destroyToken = () => window.localStorage.removeItem(ID_TOKEN_KEY);
-
-export const getUserProfile = () => {
-  const profileText = window.localStorage.getItem(USER_PROFILE);
-  let profile = {};
-  try {
-    profile = JSON.parse(profileText);
-  } catch (e) {
-    profile = {};
-  }
-  return profile;
-};

+ 0 - 3
src/views/About.vue

@@ -1,3 +0,0 @@
-<template>
-  <div class="about"><h1>This is an about page</h1></div>
-</template>

+ 0 - 15
src/views/Home.vue

@@ -1,15 +0,0 @@
-<template>
-  <div></div>
-</template>
-
-<script>
-// @ is an alias to /src
-// import HelloWorld from "@/components/HelloWorld.vue";
-
-export default {
-  name: "home"
-  // components: {
-  //   HelloWorld
-  // }
-};
-</script>

+ 51 - 42
src/views/login/Login.vue

@@ -1,19 +1,30 @@
 <template>
   <div>
     <div class="intro">Sign in</div>
+    <b-notification type="is-danger" :active.sync="isServerError">
+      {{ errorMassage }}
+    </b-notification>
     <b-field
-      label="Username"
-      :type="!$v.username.required ? 'is-danger' : ''"
-      :message="!$v.username.required ? 'username required' : ''"
+      label="邮箱"
+      :type="$v.username.$error ? 'is-danger' : ''"
+      :message="$v.username.$error ? '请输入邮箱' : ''"
     >
-      <b-input v-model="username" placeholder="input username here"></b-input>
+      <b-input
+        type="email"
+        v-model="$v.username.$model"
+        placeholder="在此输入邮箱"
+      ></b-input>
     </b-field>
 
-    <b-field label="Password">
+    <b-field
+      label="密码"
+      :type="$v.password.$error ? 'is-danger' : ''"
+      :message="$v.password.$error ? '请输入密码' : ''"
+    >
       <b-input
         type="password"
-        v-model="password"
-        placeholder="input password here"
+        v-model="$v.password.$model"
+        placeholder="在此输入密码"
         password-reveal
       >
       </b-input>
@@ -29,7 +40,7 @@
           登 陆
         </button>
         <router-link
-          :to="{ name: 'register' }"
+          :to="{ name: 'REGISTER' }"
           :class="{ 'is-text': true }"
           class="button"
           type="submit"
@@ -41,68 +52,66 @@
 </template>
 
 <script>
-import { required } from "vuelidate/lib/validators";
+import { required, email } from "vuelidate/lib/validators";
+import { login } from "@/api/auth";
+import { SET_PROFILE } from "@/store/type/mutations.type";
+import { destroyToken, saveToken } from "@/util/tokenStorage";
+import { HOME } from "@/router/name";
 
 export default {
   data() {
     return {
       username: "",
       password: "",
-      submitting: false
+      submitting: false,
+      errorMassage: null,
+      isServerError: false
     };
   },
   validations: {
     username: {
-      required
+      required,
+      email
     },
     password: {
       required
     }
   },
-  computed: {},
+  mounted() {
+    destroyToken();
+  },
   methods: {
-    loginAction() {
-      this.$router.push("");
+    async loginAction() {
+      this.$v.$touch();
+      if (!this.$v.$invalid) {
+        const { username = "", password = "" } = this;
+        try {
+          this.submitting = true;
+          const {
+            data,
+            _header: { Authorization }
+          } = await login({ username, password });
+          saveToken(Authorization);
+          this.$store.commit(SET_PROFILE, data);
+          this.$router.push({ name: HOME });
+        } catch (e) {
+          this.errorMassage = e.message;
+          this.isServerError = true;
+          this.submitting = false;
+        }
+      }
     }
   }
-
-  // @State((state: RootState) => state.auth.isLoginError)
-  // public isLoginError: boolean;
-  // public mounted() {
-  //   this.$store.dispatch(LOGOUT);
-  //   Validator.extend("truthy", {
-  //     getMessage: (field: string): string => "两次输入的密码不相同",
-  //     validate: (value: string): boolean => value === this.credentials.password
-  //   });
-  // }
-  // public closeError() {
-  //   this.$store.commit(SET_LOGIN_ERROR, false);
-  // }
-  // public submit() {
-  //   this.$validator.validateAll().then(async result => {
-  //     if (result) {
-  //       this.submitting = true;
-  //       await this.$store.dispatch(LOGIN, this.credentials);
-  //       if (!this.isLoginError) {
-  //         this.$router.push({ name: "home" });
-  //       } else {
-  //         this.submitting = false;
-  //       }
-  //     }
-  //   });
-  // }
 };
 </script>
 
 <style lang="scss" scoped>
 .intro {
-  /*<!--color: $oc-gray-7;-->*/
   text-align: center;
   font-size: 25px;
   padding: 10px 0 30px;
 }
 .link {
-  /*<!--color: $oc-blue-5;-->*/
   text-decoration: none;
 }
 .action-section {

+ 175 - 0
src/views/login/Register.vue

@@ -0,0 +1,175 @@
+<template>
+  <div>
+    <div class="intro">Sign up</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="邮箱"
+      :type="$v.username.$error ? 'is-danger' : ''"
+      :message="
+        $v.username.$error
+          ? [
+              !$v.username.email ? '邮箱格式不正确' : undefined,
+              !$v.username.required ? '请输入用户名' : undefined
+            ]
+          : ''
+      "
+    >
+      <b-input
+        v-model="$v.username.$model"
+        type="email"
+        placeholder="在此输入邮箱"
+      ></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>
+      <p class="control level" style="padding-top: 20px">
+        <button
+          style="width:40%"
+          :class="{ 'is-loading': submitting }"
+          class="button is-link"
+          @click="registerAction"
+        >
+          注 册
+        </button>
+        <router-link
+          :to="{ name: 'LOGIN' }"
+          :class="{ 'is-text': true }"
+          class="button"
+          type="submit"
+          >已有账户,登陆
+        </router-link>
+      </p>
+    </b-field>
+  </div>
+</template>
+
+<script>
+import {
+  required,
+  alphaNum,
+  minLength,
+  maxLength,
+  email,
+  sameAs
+} from "vuelidate/lib/validators";
+import { register } from "@/api/auth";
+import { LOGIN } from "@/router/name";
+
+export default {
+  data() {
+    return {
+      nickname: "",
+      username: "",
+      password: "",
+      email: "",
+      passwordAgain: "",
+      submitting: false,
+      errorMassage: null,
+      isServerError: false
+    };
+  },
+  validations: {
+    nickname: {
+      alphaNum,
+      minLength: minLength(4),
+      maxLength: maxLength(16)
+    },
+    username: {
+      required,
+      email
+    },
+    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 { username = "", password = "", nickname = null } = this;
+        try {
+          await register({ username, password, nickname });
+          this.$toast.open({
+            message: `用户「${
+              nickname === null ? username : nickname
+            }」注册成功`,
+            type: "is-success"
+          });
+          this.$router.push({ name: LOGIN });
+        } catch (e) {
+          this.errorMassage = e.message;
+          this.isServerError = true;
+          this.submitting = false;
+        }
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.intro {
+  text-align: center;
+  font-size: 25px;
+  padding: 10px 0 30px;
+}
+.link {
+  text-decoration: none;
+}
+.action-section {
+  display: flex;
+  justify-content: space-between;
+}
+</style>

+ 3 - 0
src/views/student/Code/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 3 - 0
src/views/student/Dashboard/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 3 - 0
src/views/student/Document/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 3 - 0
src/views/student/Group/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 39 - 0
src/views/student/Home/index.vue

@@ -0,0 +1,39 @@
+<template>
+  <div>
+    <section class="hero">
+      <div class="hero-body">
+        <div class="container">
+          <h1 class="title">我的课程</h1>
+          <h2 class="subtitle">展示我加入的全部课程</h2>
+        </div>
+      </div>
+    </section>
+    <div class="container">
+      <div class="columns is-multiline is-desktop">
+        <div :key="item.id" v-for="item in courses" class="column is-4">
+          <router-link :to="`/course-student/${item.id}/dashboard`">
+            <card hoverable>{{ item.name }}</card>
+          </router-link>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Card from "@/components/Card";
+import { getStudentCourses } from "@/api/course";
+export default {
+  components: {
+    Card
+  },
+  data() {
+    return {
+      courses: []
+    };
+  },
+  mounted() {
+    getStudentCourses().then(value => (this.courses = value.data));
+  }
+};
+</script>

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

@@ -0,0 +1,68 @@
+<template>
+  <div>
+    <nav class="breadcrumb" aria-label="breadcrumbs">
+      <ul>
+        <li><router-link :to="{ name: 'HOME' }">所有课程</router-link></li>
+        <li>
+          <router-link :to="`/student-course/${course.id}/dashboard`">{{
+            course.name
+          }}</router-link>
+        </li>
+        <li class="is-active">
+          <router-link :to="`/student-course/${course.id}/review`"
+            >评审作业</router-link
+          >
+        </li>
+      </ul>
+    </nav>
+    <div style="margin: 30px 0">
+      <h1 class="title">评审作业</h1>
+      <div class="subtitle">您仍剩余 {{ review.length }} 份评审作业待完成</div>
+    </div>
+
+    <div class="tabs">
+      <ul>
+        <li class="is-active"><a>待完成</a></li>
+        <li><a>需复合</a></li>
+      </ul>
+    </div>
+    <div class="columns is-multiline is-desktop">
+      <div :key="item.id" v-for="item in review" class="column is-4">
+        <card hoverable>{{ item.name }}</card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState } from "vuex";
+import { getStudentCourseReviewList } from "@/api/review";
+import Card from "@/components/Card";
+export default {
+  components: {
+    Card
+  },
+  computed: {
+    ...mapState({
+      course: state => state.course.currentCourse
+    })
+  },
+  data() {
+    return {
+      courses: [],
+      review: []
+    };
+  },
+  mounted() {
+    getStudentCourseReviewList(this.courses.id).then(
+      value => (this.review = value.data)
+    );
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.top-nav-section {
+  padding: 0 40px;
+}
+</style>

+ 3 - 0
src/views/teacher/Dashboard/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div></div>
+</template>

+ 39 - 0
src/views/teacher/Home/index.vue

@@ -0,0 +1,39 @@
+<template>
+  <div>
+    <section class="hero">
+      <div class="hero-body">
+        <div class="container">
+          <h1 class="title">我的课程</h1>
+          <h2 class="subtitle">展示我开设的全部课程</h2>
+        </div>
+      </div>
+    </section>
+    <div class="container">
+      <div class="columns is-multiline is-desktop">
+        <div :key="item.id" v-for="item in courses" class="column is-4">
+          <router-link :to="`/course-teacher/${item.id}/dashboard`">
+            <card hoverable>{{ item.name }}</card>
+          </router-link>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Card from "@/components/Card";
+import { getTeacherCourses } from "@/api/course";
+export default {
+  components: {
+    Card
+  },
+  data() {
+    return {
+      courses: []
+    };
+  },
+  mounted() {
+    getTeacherCourses().then(value => (this.courses = value.data));
+  }
+};
+</script>