dusenyao 4 سال پیش
والد
کامیت
e2e277b017

+ 0 - 2
.browserslistrc

@@ -1,2 +0,0 @@
-> 1%
-last 2 versions

+ 2 - 2
.vscode/launch.json

@@ -8,7 +8,7 @@
       "type": "chrome",
       "request": "launch",
       "name": "vuejs: chrome",
-      "url": "http://localhost:9217",
+      "url": "http://localhost:7878",
       "webRoot": "${workspaceFolder}/src",
       "breakOnLoad": true,
       "sourceMapPathOverrides": {
@@ -21,7 +21,7 @@
       "type": "firefox",
       "request": "launch",
       "name": "vuejs: firefox",
-      "url": "http://localhost:9217",
+      "url": "http://localhost:7878",
       "webRoot": "${workspaceFolder}/src",
       "pathMappings": [{ "url": "webpack:///src/", "path": "${webRoot}/" }]
     }

+ 20 - 16
README.md

@@ -1,29 +1,33 @@
 # gcls_sys_learn_web
 
+> 全球中文学习系统 教学系统 Web端
+
 ## Project setup
-```
-npm install
-```
 
-### Compiles and hot-reloads for development
-```
+```bash
+# 克隆项目
+git clone http://gcls-git.helxsoft.cn/dsy/gcls_sys_learn_web.git
+
+# 启动服务
 npm run serve
 ```
 
-### Compiles and minifies for production
-```
+## 发布
+
+```bash
+# 构建测试环境
+build:stage
+
+# 构建生产环境
 npm run build
 ```
 
-### Run your unit tests
-```
-npm run test:unit
-```
+### 其他
 
-### Lints and fixes files
-```
+```bash
+# 代码格式检查
 npm run lint
-```
 
-### Customize configuration
-See [Configuration Reference](https://cli.vuejs.org/config/).
+# 代码格式检查并自动修复
+npm run lint -- --fix
+```

+ 4 - 9
package.json

@@ -56,15 +56,10 @@
     "vue-loader": "^15.9.6",
     "vue-template-compiler": "^2.6.12"
   },
-  "gitHooks": {
-    "pre-commit": "lint-staged"
-  },
-  "lint-staged": {
-    "*.{js,jsx,vue}": [
-      "vue-cli-service lint",
-      "git add"
-    ]
-  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions"
+  ],
   "engines": {
     "node": ">=8.9",
     "npm": ">= 3.0.0"

+ 9 - 0
src/api/table.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+export function getList(params) {
+  return request({
+    url: '',
+    method: 'post',
+    params
+  });
+}

+ 9 - 0
src/api/user.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+export function login(data) {
+  return request({
+    url: '',
+    method: 'post',
+    data
+  });
+}

+ 30 - 5
src/layouts/components/Header.vue

@@ -1,20 +1,45 @@
 <template>
   <div>
     <el-header>
-      <span></span>
+      <el-row>
+        <el-col :span="1">
+          <span>logo</span>
+        </el-col>
+        <el-col :span="1" :push="22"
+          ><div class="sign-out" @click="signOut">{{ userType }}退出</div></el-col
+        >
+      </el-row>
     </el-header>
   </div>
 </template>
 
 <script>
-export default {};
+export default {
+  props: {
+    userType: {
+      type: String,
+      default: ''
+    }
+  },
+  methods: {
+    async signOut() {
+      await this.$store.dispatch('user/signOut');
+      this.$router.push(`/login?redirect=${this.$route.fullPath}`);
+    }
+  }
+};
 </script>
 
 <style lang="scss" scoped>
-$header-h: 49px;
-
 .el-header {
-  height: $header-h;
+  height: $header-h !important;
   line-height: $header-h;
+  background-color: $bacColor;
+  text-align: center;
+
+  .sign-out {
+    cursor: pointer;
+    font-size: 12px;
+  }
 }
 </style>

+ 15 - 18
src/layouts/index.vue

@@ -1,6 +1,6 @@
 <template>
-  <div>
-    <Header />
+  <div class="app-container">
+    <Header :user-type="userType" />
     <section class="app-main">
       <transition name="fade-transfrom" mode="out-in">
         <router-view :key="key"></router-view>
@@ -18,29 +18,26 @@ export default {
   },
   computed: {
     key() {
-      console.log(this.$route.path);
       return this.$route.path;
+    },
+    userType() {
+      return this.$store.state.user.userType === 'teacher' ? '教师' : '学员';
     }
   }
 };
 </script>
 
-<style scoped>
-.app-main {
-  /* 50 = navbar  */
-  min-height: calc(100vh - 50px);
-  width: 100%;
-  position: relative;
-  overflow: hidden;
-  padding-top: 50px;
-}
-</style>
+<style lang="scss" scoped>
+$top: 31px;
 
-<style lang="scss">
-// fix css style bug in open el-dialog
-.el-popup-parent--hidden {
-  .fixed-header {
-    padding-right: 15px;
+.app-container {
+  height: 100%;
+  .app-main {
+    min-height: calc(100vh - #{$header-h + $top});
+    width: 100%;
+    position: relative;
+    overflow: hidden;
+    padding-top: $top;
   }
 }
 </style>

+ 2 - 0
src/main.js

@@ -9,6 +9,8 @@ import 'element-ui/lib/theme-chalk/index.css';
 import '@/styles/index.scss'; // global css
 import 'normalize.css/normalize.css';
 
+import '@/permission'; // 权限控制
+
 Vue.use(ElementUI);
 
 Vue.config.productionTip = false;

+ 36 - 0
src/permission.js

@@ -0,0 +1,36 @@
+import router from './router';
+import { getToken } from '@/utils/auth';
+
+import NProgress from 'nprogress';
+import 'nprogress/nprogress.css';
+
+NProgress.configure({ showSpinner: false });
+
+const whiteList = ['/login']; // 重定向白名单
+
+// 全局前置守卫
+router.beforeEach(async (to, from, next) => {
+  NProgress.start();
+
+  const hasToken = getToken();
+  if (hasToken) {
+    if (to.path === '/login') {
+      next({ path: '/' });
+      NProgress.done();
+    } else {
+      next();
+    }
+  } else if (whiteList.indexOf(to.path) !== -1) {
+    // 在登录白名单中,直接进入
+    next();
+  } else {
+    // 其他无权访问的页面将重定向到登录页面
+    next(`/login?redirect=${to.path}`);
+    NProgress.done();
+  }
+});
+
+// 全局后置钩子
+router.afterEach(() => {
+  NProgress.done();
+});

+ 2 - 20
src/router/index.js

@@ -4,11 +4,6 @@ import VueRouter from 'vue-router';
 import Layout from '@/layouts';
 import Login from '@/views/login';
 
-import NProgress from 'nprogress'; // progress bar
-import 'nprogress/nprogress.css';
-
-NProgress.configure({ showSpinner: false });
-
 Vue.use(VueRouter);
 
 const routes = [
@@ -34,8 +29,7 @@ const routes = [
   },
   {
     path: '*',
-    redirect: '/404',
-    component: () => import('@/views/404')
+    redirect: '/404'
   }
 ];
 
@@ -48,22 +42,10 @@ const createRouter = () =>
 
 const router = createRouter();
 
-// 全局前置守卫
-router.beforeEach(async (to, from, next) => {
-  NProgress.start();
-  next();
-});
-
-// 全局后置钩子
-router.afterEach(() => {
-  NProgress.done();
-});
-
 // 重置路由
 export function resetRouter() {
   const newRouter = createRouter();
-
-  router.matcher = newRouter.matcher; // reset router
+  router.matcher = newRouter.matcher;
 }
 
 export default router;

+ 5 - 0
src/store/getters.js

@@ -0,0 +1,5 @@
+const getters = {
+  token: state => state.user.state
+};
+
+export default getters;

+ 8 - 1
src/store/index.js

@@ -1,5 +1,8 @@
 import Vue from 'vue';
 import Vuex from 'vuex';
+import getters from './getters';
+import user from './modules/user';
+import app from './modules/app';
 
 Vue.use(Vuex);
 
@@ -7,5 +10,9 @@ export default new Vuex.Store({
   state: {},
   mutations: {},
   actions: {},
-  modules: {}
+  modules: {
+    user,
+    app
+  },
+  getters
 });

+ 12 - 0
src/store/modules/app.js

@@ -0,0 +1,12 @@
+const state = {};
+
+const mutations = {};
+
+const actions = {};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+};

+ 68 - 0
src/store/modules/user.js

@@ -0,0 +1,68 @@
+import { getToken, removeToken, setToken } from '@/utils/auth';
+import { user } from '@/store/mutation-types';
+import { resetRouter } from '@/router';
+
+const getDefaultSate = () => {
+  return {
+    token: getToken(),
+    name: 'teacher',
+    userType: ''
+  };
+};
+
+const state = getDefaultSate();
+
+const mutations = {
+  [user.RESET_STATE]: state => {
+    Object.assign(state, getDefaultSate());
+  },
+  [user.SET_TOKEN]: (state, token) => {
+    state.token = token;
+  },
+  [user.SET_USER_TYPE]: (state, userType) => {
+    state.userType = userType;
+  }
+};
+
+const actions = {
+  // 登录
+  login({ commit }, userInfo) {
+    const {
+      loginForm: { username, password },
+      userType
+    } = userInfo;
+    return new Promise(reslove => {
+      if (Boolean(username) && Boolean(password)) {
+        setToken(`token-${username}-${password}`);
+        commit(user.SET_USER_TYPE, userType);
+        commit(user.SET_TOKEN, `token-${username}-${password}`);
+        reslove();
+      }
+    });
+  },
+
+  // 用户退出
+  signOut({ commit }) {
+    return new Promise(resolve => {
+      removeToken();
+      resetRouter();
+      commit(user.RESET_STATE);
+      resolve();
+    });
+  },
+
+  removeToken({ commit }) {
+    return new Promise(reslove => {
+      removeToken();
+      commit(user.RESET_STATE);
+      reslove();
+    });
+  }
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+};

+ 9 - 0
src/store/mutation-types.js

@@ -0,0 +1,9 @@
+const user = {
+  RESET_STATE: 'RESET_STATE',
+  SET_TOKEN: 'SET_TOKEN',
+  SET_USER_TYPE: 'SET_USER_TYPE'
+};
+
+const app = {};
+
+export { user, app };

+ 2 - 7
src/styles/index.scss

@@ -6,8 +6,8 @@ body {
   -webkit-font-smoothing: antialiased;
   text-rendering: optimizeLegibility;
   /* stylelint-disable-next-line */
-  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
-    Microsoft YaHei, Arial, sans-serif; /* stylelint-disable-line */
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial,
+    sans-serif;
 }
 
 label {
@@ -40,8 +40,3 @@ a:hover {
 div:focus {
   outline: none;
 }
-
-// main-container global css
-.app-container {
-  padding: 20px;
-}

+ 3 - 1
src/styles/variables.scss

@@ -1 +1,3 @@
-$bacColor: rgb(229, 233, 242);
+$header-h: 49px;
+$bacColor: #c4c4c4;
+$borderColor: #999;

+ 15 - 0
src/utils/auth.js

@@ -0,0 +1,15 @@
+import Cookies from 'js-cookie';
+
+const TokenKey = 'gcls_sys_learn_web';
+
+export function getToken() {
+  return Cookies.get(TokenKey);
+}
+
+export function setToken(token) {
+  return Cookies.set(TokenKey, token);
+}
+
+export function removeToken() {
+  return Cookies.remove(TokenKey);
+}

+ 43 - 0
src/utils/request.js

@@ -0,0 +1,43 @@
+import axios from 'axios';
+import { Message } from 'element-ui';
+
+axios.defaults.withCredentials = true; // 跨域请求时是否需要使用凭证
+axios.defaults.dataType = 'json';
+axios.defaults.headers['cache-control'] = 'no-cache';
+axios.defaults.headers['Content-Type'] = 'application/json';
+axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest';
+
+const service = axios.create({
+  timeout: 10000
+});
+
+// 请求拦截器
+service.interceptors.request.use({});
+
+//响应拦截器
+service.interceptors.response.use(
+  response => {
+    const res = response.data;
+
+    if (res.code === 404) {
+      Message({
+        message: '请求的资源不存在',
+        type: 'error',
+        duration: 3 * 1000
+      });
+      return Promise.reject(new Error(res.message || 'Error'));
+    }
+
+    return res;
+  },
+  error => {
+    Message({
+      message: error.message,
+      type: 'error',
+      duration: 3 * 1000
+    });
+    return Promise.reject(error);
+  }
+);
+
+export default service;

+ 5 - 1
src/views/404.vue

@@ -12,7 +12,11 @@ export default {
 
 <style lang="scss">
 .http-404 {
+  transform: translate(-50%, -50%);
+  position: absolute;
+  top: 40%;
+  left: 50%;
   color: rgb(0, 174, 255);
-  text-align: center;
+  font-size: 32px;
 }
 </style>

+ 85 - 8
src/views/login/index.vue

@@ -1,29 +1,56 @@
 <template>
   <div class="login-container">
-    <el-form ref="loginForm" class="login-form" auto-complete="on" label-position="left">
+    <el-form
+      ref="loginForm"
+      :model="loginForm"
+      :rules="loginRules"
+      class="login-form"
+      auto-complete="on"
+      label-position="left"
+    >
       <div class="title-container">
         <h3 class="title">系统登录</h3>
       </div>
 
-      <el-form-item>
-        <el-input placeholder="用户名" />
+      <el-form-item prop="username">
+        <el-input
+          type="text"
+          v-model="loginForm.username"
+          ref="username"
+          name="username"
+          auto-complete="on"
+          placeholder="用户名"
+        />
       </el-form-item>
-      <el-form-item>
-        <el-input placeholder="密码" />
+
+      <el-form-item prop="password">
+        <el-input
+          type="password"
+          v-model="loginForm.password"
+          ref="password"
+          name="password"
+          placeholder="密码"
+          auto-complete="on"
+          @keyup.enter.native="handleLogin"
+        />
       </el-form-item>
+
       <el-row>
         <el-col :span="12">
           <el-button
             class="login-button"
             type="primary"
+            :loading="loading"
             @click.native.prevent="handleLogin('teacher')"
             >教师登录</el-button
           >
         </el-col>
+
         <el-col :span="12">
           <el-button
             class="login-button"
             type="primary"
+            :loading="loading"
             @click.native.prevent="handleLogin('student')"
             >学员登录</el-button
           >
@@ -36,11 +63,61 @@
 <script>
 export default {
   data() {
-    return {};
+    const validateUsername = (rule, value, callback) => {
+      if (!value) {
+        callback(new Error('请填写用户名!'));
+      } else {
+        callback();
+      }
+    };
+    const validatePassword = (rule, value, callback) => {
+      if (value.length < 6) {
+        callback(new Error('密码不能少于六位'));
+      } else {
+        callback();
+      }
+    };
+
+    return {
+      loginForm: {
+        username: 'teacher',
+        password: '1234567a'
+      },
+      loginRules: {
+        username: [{ require: true, trigger: 'blur', validator: validateUsername }],
+        password: [{ require: true, trigger: 'blur', validator: validatePassword }]
+      },
+      loading: false,
+      redirect: null
+    };
+  },
+  watch: {
+    $router: {
+      handler: function (router) {
+        this.redirect = router.query && router.query.redirect;
+      }
+    }
   },
   methods: {
-    handleLogin(loginType) {
-      console.log(loginType);
+    handleLogin(userType) {
+      this.$refs.loginForm.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          this.$store
+            .dispatch('user/login', { loginForm: this.loginForm, userType })
+            .then(() => {
+              this.$router.push({ path: this.redirect || '/' });
+              this.loading = false;
+              this.$message.success('登录成功!');
+            })
+            .catch(() => {
+              this.loading = false;
+              this.$message.error('登录失败!');
+            });
+        } else {
+          return false;
+        }
+      });
     }
   }
 };

+ 1 - 1
src/views/teacher/TaskKanban.vue

@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div class="http-404">
     <h1>Task</h1>
   </div>
 </template>

+ 1 - 1
stylelint.config.js

@@ -1,5 +1,5 @@
 module.exports = {
-  defaultSeverity: 'error',
+  defaultSeverity: 'warning',
   extends: 'stylelint-config-standard',
   plugins: ['stylelint-declaration-block-no-ignored-properties', 'stylelint-order'],
   rules: {

+ 5 - 4
vue.config.js

@@ -9,7 +9,7 @@ const name = '全球中文学习系统 教学系统 Web端';
 
 const NODE_ENV = process.env.NODE_ENV;
 
-const port = process.env.port || process.env.npm_config_port || 9217;
+const port = process.env.port || process.env.npm_config_port || 7878;
 
 // 配置项说明 https://cli.vuejs.org/config/
 module.exports = {
@@ -31,7 +31,7 @@ module.exports = {
     loaderOptions: {
       scss: {
         // 为 scss 配置共享全局变量
-        additionalData: '@import "./src/styles/variables.scss";' //注意
+        additionalData: '@import "./src/styles/variables.scss";'
       }
     }
   },
@@ -44,6 +44,7 @@ module.exports = {
       }
     },
     devtool: NODE_ENV === 'development' ? 'source-map' : '',
+    // stylelint 配置
     plugins: [
       new StyleLintPlugin({
         files: ['**/*.{vue,htm,html,css,sss,less,scss,sass}'],
@@ -66,7 +67,7 @@ module.exports = {
       }
     ]);
 
-    // 当页面很多时,它将导致太多无意义的请求
+    // 当页面很多时,prefetch 将导致太多无意义的请求,开启这个
     // config.plugins.delete('prefetch');
 
     // 设置 svg-sprite-loader
@@ -83,7 +84,7 @@ module.exports = {
       })
       .end();
 
-    config.when(process.env.NODE_ENV !== 'development', () => {
+    config.when(NODE_ENV !== 'development', () => {
       config
         .plugin('ScriptExtHtmlWebpackPlugin')
         .after('html')