浏览代码

[新增]登陆注册 && 课程视图

WuXinyu 7 年之前
父节点
当前提交
469ecd09a0

+ 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 - 7
src/App.vue

@@ -1,11 +1,5 @@
 <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">

+ 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
     }
   });
 };

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

@@ -1,28 +1,16 @@
 @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
+.has-text-primary{
+  color:#209cee !important;
+}
+
+.input{
+  box-shadow: none !important;
+}

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

@@ -0,0 +1,61 @@
+<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>
+.custom-card {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  background: #ffffff;
+  border: 2px solid #b9c7d8;
+}
+
+.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>

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

@@ -0,0 +1,85 @@
+<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>
+.nav-section {
+  border-bottom: 2px solid #b9c7d8;
+}
+
+@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;-->*/

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


+ 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 = "TEACHER_COURSE";

+ 83 - 0
src/router/routes.js

@@ -0,0 +1,83 @@
+import { HOME, LOGIN, REGISTER, STUDENT, TEACHER } from "@/router/name";
+import { judgeLogin } from "@/util/auth";
+import { getUserProfile } from "@/util/profileStorage";
+
+export default [
+  {
+    path: "/",
+    name: HOME,
+    redirect: () => {
+      if (judgeLogin()) {
+        const { role } = getUserProfile();
+        return { name: role };
+      } else {
+        return { name: LOGIN };
+      }
+    }
+  },
+  {
+    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: "/teacher-course",
+    component: () => import("@/layouts/HomeLayout"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: TEACHER,
+        path: "",
+        component: () => import("@/views/teacher/Home")
+      }
+    ]
+  },
+  {
+    path: "/student-course",
+    component: () => import("@/layouts/HomeLayout"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: TEACHER,
+        path: "",
+        component: () => import("@/views/teacher/Home")
+      }
+    ]
+  },
+  {
+    path: "/student",
+    component: () => import("@/layouts/HomeLayout"),
+    meta: { requiresAuth: true },
+    children: [
+      {
+        name: STUDENT,
+        path: "",
+        component: () => import("@/views/student/Home")
+      }
+    ]
+  }
+];

+ 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) {
-    profile = {};
+    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>

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


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

@@ -0,0 +1,37 @@
+<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">
+          <card hoverable>{{ item.name }}</card>
+        </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>

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


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

@@ -0,0 +1,37 @@
+<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">
+          <card hoverable>{{ item.name }}</card>
+        </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>