李志伟 3 vuotta sitten
vanhempi
commit
4bf1a9028f

+ 60
- 0
src/api/oss.js Näytä tiedosto

@@ -0,0 +1,60 @@
1
+import OSS from 'ali-oss'
2
+import request from '@/utils/request'
3
+
4
+let client
5
+let stsInfo
6
+
7
+/**
8
+ * 获取 OSS-STS 临时凭证
9
+ * @returns
10
+ */
11
+export const getSTSToken = () => request({ url: '/admin/oss-sts' })
12
+
13
+/**
14
+ * 获取 OSS 客户端
15
+ * @returns
16
+ */
17
+export function getOSSClient() {
18
+  if (client) {
19
+    return Promise.resolve(client)
20
+  }
21
+
22
+  return getSTSToken().then((sts) => {
23
+    stsInfo = sts
24
+    client = new OSS({
25
+      accessKeyId: sts.data.accessKeyId,
26
+      accessKeySecret: sts.data.accessKeySecret,
27
+      stsToken: sts.data.stsToken,
28
+      bucket: sts.data.bucket,
29
+      endpoint: sts.data.endpoint,
30
+      refreshSTSTokenInterval: 25 * 60 * 1000, // 每25min刷新一次
31
+      refreshSTSToken: () => {
32
+        return getSTSToken().then((res) => {
33
+          return {
34
+            accessKeyId: res.accessKeyId,
35
+            accessKeySecret: res.accessKeySecret,
36
+            stsToken: res.stsToken
37
+          }
38
+        })
39
+      }
40
+    })
41
+
42
+    return client
43
+  })
44
+}
45
+
46
+/**
47
+ * 上传文件到 OSS
48
+ * @param {*} file
49
+ * @returns
50
+ */
51
+export function upload(file) {
52
+  return getOSSClient().then(() => {
53
+    const data = new FormData()
54
+    data.append('file', file)
55
+    const now = new Date()
56
+    const fileName = `${stsInfo.path}/${now.valueOf()}-${file.name}`
57
+
58
+    return client.put(fileName, file).then((res) => res.url)
59
+  })
60
+}

+ 21
- 0
src/api/signup.js Näytä tiedosto

@@ -0,0 +1,21 @@
1
+import request from '@/utils/request'
2
+
3
+/**
4
+* 注册账号
5
+* @param {*} data
6
+* @returns
7
+*/
8
+export const signUp = (data) => request({
9
+  url: '/admin/signup',
10
+  method: 'post',
11
+  data
12
+})
13
+
14
+/**
15
+ * 获取验证码
16
+ * @param {*} params
17
+ * @returns
18
+ */
19
+export const getCaptcha = (params) => request({
20
+  url: '/admin/captcha', params
21
+})

+ 9
- 0
src/api/user.js Näytä tiedosto

@@ -0,0 +1,9 @@
1
+import request from '@/utils/request'
2
+
3
+export function login(data) {
4
+  return request({
5
+    url: '/admin/login',
6
+    method: 'post',
7
+    data
8
+  })
9
+}

+ 29
- 0
src/layout/components/Navbar.vue Näytä tiedosto

@@ -3,6 +3,31 @@
3 3
     <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
4 4
 
5 5
     <breadcrumb class="breadcrumb-container" />
6
+
7
+    <div class="right-menu">
8
+      <el-dropdown class="avatar-container" trigger="click">
9
+        <div class="avatar-wrapper">
10
+          <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
11
+          <i class="el-icon-caret-bottom" />
12
+        </div>
13
+        <el-dropdown-menu slot="dropdown" class="user-dropdown">
14
+          <router-link to="/">
15
+            <el-dropdown-item>
16
+              Home
17
+            </el-dropdown-item>
18
+          </router-link>
19
+          <a target="_blank" href="https://github.com/PanJiaChen/vue-admin-template/">
20
+            <el-dropdown-item>Github</el-dropdown-item>
21
+          </a>
22
+          <a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
23
+            <el-dropdown-item>Docs</el-dropdown-item>
24
+          </a>
25
+          <el-dropdown-item divided @click.native="logout">
26
+            <span style="display:block;">Log Out</span>
27
+          </el-dropdown-item>
28
+        </el-dropdown-menu>
29
+      </el-dropdown>
30
+    </div>
6 31
   </div>
7 32
 </template>
8 33
 
@@ -25,6 +50,10 @@ export default {
25 50
   methods: {
26 51
     toggleSideBar() {
27 52
       this.$store.dispatch('app/toggleSideBar')
53
+    },
54
+    async logout() {
55
+      await this.$store.dispatch('user/logout')
56
+      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
28 57
     }
29 58
   }
30 59
 }

+ 3
- 0
src/main.js Näytä tiedosto

@@ -13,6 +13,8 @@ import store from './store'
13 13
 import router from './router'
14 14
 
15 15
 import '@/icons' // icon
16
+import '@/permission' // permission control
17
+
16 18
 /**
17 19
  * If you don't want to use mock-server
18 20
  * you want to use MockJs for mock api
@@ -30,6 +32,7 @@ import '@/icons' // icon
30 32
 // Vue.use(ElementUI, { locale })
31 33
 // 如果想要中文版 element-ui,按如下方式声明
32 34
 Vue.use(ElementUI)
35
+
33 36
 Vue.config.productionTip = false
34 37
 
35 38
 new Vue({

+ 60
- 0
src/permission.js Näytä tiedosto

@@ -0,0 +1,60 @@
1
+import router from './router'
2
+import store from './store'
3
+import { Message } from 'element-ui'
4
+import NProgress from 'nprogress' // progress bar
5
+import 'nprogress/nprogress.css' // progress bar style
6
+import { getToken } from '@/utils/auth' // get token from cookie
7
+import getPageTitle from '@/utils/get-page-title'
8
+
9
+NProgress.configure({ showSpinner: false }) // NProgress Configuration
10
+
11
+const whiteList = ['/login', '/login/forgotPassword', '/login/register'] // no redirect whitelist
12
+
13
+router.beforeEach(async(to, from, next) => {
14
+  // start progress bar
15
+  NProgress.start()
16
+
17
+  // set page title
18
+  document.title = getPageTitle(to.meta.title)
19
+
20
+  // determine whether the user has logged in
21
+  const hasToken = getToken()
22
+  if (hasToken) {
23
+    if (to.path === '/login') {
24
+      // if is logged in, redirect to the home page
25
+      next({ path: '/' })
26
+      NProgress.done()
27
+    } else {
28
+      const hasGetUserInfo = store.getters.name
29
+      if (hasGetUserInfo) {
30
+        next()
31
+      } else {
32
+        try {
33
+          next()
34
+        } catch (error) {
35
+          // remove token and go to login page to re-login
36
+          await store.dispatch('user/resetToken')
37
+          Message.error(error || 'Has Error')
38
+          next(`/login?redirect=${to.path}`)
39
+          NProgress.done()
40
+        }
41
+      }
42
+    }
43
+  } else {
44
+    /* has no token*/
45
+
46
+    if (whiteList.indexOf(to.path) !== -1) {
47
+      // in the free login whitelist, go directly
48
+      next()
49
+    } else {
50
+      // other pages that do not have permission to access are redirected to the login page.
51
+      next(`/login?redirect=${to.path}`)
52
+      NProgress.done()
53
+    }
54
+  }
55
+})
56
+
57
+router.afterEach(() => {
58
+  // finish progress bar
59
+  NProgress.done()
60
+})

+ 21
- 0
src/router/index.js Näytä tiedosto

@@ -72,11 +72,32 @@ export const constantRoutes = [
72 72
     ]
73 73
   },
74 74
 
75
+  {
76
+    path: '/login',
77
+    component: () => import('@/views/login/index'),
78
+    hidden: true
79
+  },
80
+
81
+  {
82
+    path: '/login/register',
83
+    component: () => import('@/views/login/register'),
84
+    hidden: true,
85
+    name: 'register'
86
+  },
87
+
88
+  {
89
+    path: '/login/forgotPassword',
90
+    component: () => import('@/views/login/forgotPassword'),
91
+    hidden: true,
92
+    name: 'forgotPassword'
93
+  },
94
+
75 95
   {
76 96
     path: '/404',
77 97
     component: () => import('@/views/404'),
78 98
     hidden: true
79 99
   },
100
+
80 101
   {
81 102
     path: 'external-link',
82 103
     component: Layout,

+ 4
- 1
src/store/getters.js Näytä tiedosto

@@ -1,5 +1,8 @@
1 1
 const getters = {
2 2
   sidebar: state => state.app.sidebar,
3
-  device: state => state.app.device
3
+  device: state => state.app.device,
4
+  token: state => state.user.token,
5
+  avatar: state => state.user.avatar,
6
+  name: state => state.user.name
4 7
 }
5 8
 export default getters

+ 2
- 0
src/store/index.js Näytä tiedosto

@@ -3,6 +3,7 @@ import Vuex from 'vuex'
3 3
 import getters from './getters'
4 4
 import app from './modules/app'
5 5
 import settings from './modules/settings'
6
+import user from './modules/user'
6 7
 import game from './modules/game'
7 8
 
8 9
 Vue.use(Vuex)
@@ -11,6 +12,7 @@ const store = new Vuex.Store({
11 12
   modules: {
12 13
     app,
13 14
     settings,
15
+    user,
14 16
     game
15 17
   },
16 18
   getters

+ 67
- 0
src/store/modules/user.js Näytä tiedosto

@@ -0,0 +1,67 @@
1
+import { login } from '@/api/user'
2
+import { getToken, setToken, removeToken, setUserId } from '@/utils/auth'
3
+import { resetRouter } from '@/router'
4
+
5
+const getDefaultState = () => {
6
+  return {
7
+    token: getToken()
8
+  }
9
+}
10
+
11
+const state = getDefaultState()
12
+
13
+// 定义Mutations
14
+const mutations = {
15
+  RESET_STATE: (state) => {
16
+    Object.assign(state, getDefaultState())
17
+  },
18
+  SET_TOKEN: (state, token) => {
19
+    state.token = token
20
+  }
21
+}
22
+// 在actions里面增加一个才可以在页面加
23
+// this.$store.dispatch('user/login', this.loginForm)调用api里面的方法
24
+const actions = {
25
+  // user login
26
+  login({ commit }, userInfo) {
27
+    const { userName, password } = userInfo
28
+    return new Promise((resolve, reject) => {
29
+      login({ userName: userName.trim(), password: password }).then(response => {
30
+        const { data } = response
31
+        commit('SET_TOKEN', data.token)
32
+        setToken(data.token)
33
+        setUserId('admin')
34
+        resolve()
35
+      }).catch(error => {
36
+        reject(error)
37
+      })
38
+    })
39
+  },
40
+
41
+  // user logout
42
+  logout({ commit, state }) {
43
+    return new Promise((resolve, reject) => {
44
+      removeToken() // must remove  token  first
45
+      resetRouter()
46
+      commit('RESET_STATE')
47
+      resolve()
48
+    })
49
+  },
50
+
51
+  // remove token
52
+  resetToken({ commit }) {
53
+    return new Promise(resolve => {
54
+      removeToken() // must remove  token  first
55
+      commit('RESET_STATE')
56
+      resolve()
57
+    })
58
+  }
59
+}
60
+
61
+export default {
62
+  namespaced: true,
63
+  state,
64
+  mutations,
65
+  actions
66
+}
67
+

+ 25
- 0
src/utils/auth.js Näytä tiedosto

@@ -0,0 +1,25 @@
1
+import Cookies from 'js-cookie'
2
+
3
+const TokenKey = 'token'
4
+
5
+export function getToken() {
6
+  return Cookies.get(TokenKey)
7
+}
8
+
9
+export function setToken(token) {
10
+  return Cookies.set(TokenKey, token)
11
+}
12
+
13
+
14
+export function removeToken() {
15
+  return Cookies.remove(TokenKey)
16
+}
17
+
18
+const UserIdKey = 'user'
19
+
20
+export function setUserId(userId) {
21
+  return Cookies.set(UserIdKey, userId)
22
+}
23
+export function getUserId() {
24
+  return Cookies.get(UserIdKey)
25
+}

+ 4
- 0
src/utils/request.js Näytä tiedosto

@@ -1,6 +1,7 @@
1 1
 import axios from 'axios'
2 2
 import { Message } from 'element-ui'
3 3
 import store from '@/store'
4
+import { getToken, getUserId } from '@/utils/auth'
4 5
 import { downloadBlob } from './download'
5 6
 
6 7
 // create an axios instance
@@ -19,6 +20,8 @@ service.interceptors.request.use(
19 20
       // let each request carry token
20 21
       // ['X-Token'] is a custom headers key
21 22
       // please modify it according to the actual situation
23
+      config.headers['X-Authorization-JWT'] = getToken()
24
+      config.headers['x-userid'] = getUserId()
22 25
     }
23 26
     return config
24 27
   },
@@ -43,6 +46,7 @@ service.interceptors.response.use(
43 46
    */
44 47
   response => {
45 48
     const res = response.data
49
+    // if the custom code is not 20000, it is judged as an error.
46 50
     const contextType = response.headers['content-type']
47 51
     if (contextType.indexOf('application/vnd.ms-excel') > -1) {
48 52
       const data = new Blob([res])

+ 17
- 0
src/utils/upload.js Näytä tiedosto

@@ -0,0 +1,17 @@
1
+import { upload } from '@/api/oss'
2
+
3
+/**
4
+ * 获取 OSS 客户端
5
+ * @returns
6
+ */
7
+export function uploadFile({ file, onSuccess, onError }) {
8
+  upload(file).then((url) => {
9
+    onSuccess(url, file)
10
+  }).catch((e) => {
11
+    onError(e)
12
+  })
13
+
14
+  return {
15
+    abort: () => {}
16
+  }
17
+}

+ 261
- 0
src/views/login/forgotPassword.vue Näytä tiedosto

@@ -0,0 +1,261 @@
1
+<template>
2
+  <div class="login-container">
3
+    <el-form
4
+      ref="regisiterForm"
5
+      :model="regisiterForm"
6
+      :rules="regisiterRules"
7
+      class="login-form"
8
+      auto-complete="on"
9
+      label-position="left"
10
+    >
11
+      <div class="title-container">
12
+        <h3 class="title">找回密码</h3>
13
+      </div>
14
+
15
+      <el-form-item prop="phone">
16
+        <span class="svg-container">
17
+          <svg-icon icon-class="password" />
18
+        </span>
19
+        <el-input
20
+          ref="phone"
21
+          v-model="regisiterForm.phone"
22
+          type="tel"
23
+          placeholder="手机号"
24
+          name="phone"
25
+          tabindex="1"
26
+        />
27
+      </el-form-item>
28
+
29
+      <el-form-item prop="captcha" style="width:65%">
30
+        <span class="svg-container">
31
+          <svg-icon icon-class="password" />
32
+        </span>
33
+        <el-input
34
+          ref="captcha"
35
+          v-model="regisiterForm.captcha"
36
+          type="text"
37
+          placeholder="验证码"
38
+          tabindex="2"
39
+          name="captcha"
40
+        />
41
+        <el-button style="position: absolute;right: -150px;top: 6px;" :disabled="disabled" @click="sendCode">{{ btnText }}</el-button>
42
+      </el-form-item>
43
+      <el-form-item prop="password">
44
+        <span class="svg-container">
45
+          <svg-icon icon-class="password" />
46
+        </span>
47
+        <el-input
48
+          ref="password"
49
+          v-model="regisiterForm.password"
50
+          type="text"
51
+          placeholder="密码"
52
+          name="password"
53
+          tabindex="3"
54
+          @keyup.enter.native="handleLogin"
55
+        />
56
+      </el-form-item>
57
+      <el-button
58
+        :loading="loading"
59
+        type="primary"
60
+        style="width: 100%; margin-bottom: 30px"
61
+        @click.native.prevent="handleLogin"
62
+      >找回密码</el-button>
63
+    </el-form>
64
+  </div>
65
+</template>
66
+<script>
67
+import { getCaptcha, signUp } from '@/api/signup'
68
+import md5 from 'js-md5'
69
+export default {
70
+  name: 'Login',
71
+  data() {
72
+    const validatePhone = (rule, value, callback) => {
73
+      if (!value) {
74
+        return callback(new Error('手机号不能为空'))
75
+      } else {
76
+        const reg = /^1[3|4|5|7|8][0-9]\d{8}$/
77
+        if (reg.test(value)) {
78
+          callback()
79
+        } else {
80
+          return callback(new Error('请输入正确的手机号'))
81
+        }
82
+      }
83
+    }
84
+    return {
85
+      regisiterForm: {
86
+        password: '',
87
+        phone: undefined,
88
+        captcha: undefined
89
+      },
90
+      regisiterRules: {
91
+        password: [
92
+          { required: true, trigger: 'change', message: '请输入密码' }
93
+        ],
94
+        phone: [
95
+          { required: true, trigger: 'change', validator: validatePhone }
96
+        ],
97
+        captcha: [
98
+          { required: true, trigger: 'change', message: '请输入验证码' }
99
+        ]
100
+      },
101
+      disabled: false,
102
+      btnText: '发送验证码',
103
+      time: 0,
104
+      loading: false
105
+    }
106
+  },
107
+  methods: {
108
+    sendCode() {
109
+      this.disabled = true
110
+
111
+      this.$refs.regisiterForm.validateField('phone', (errorMessage) => {
112
+        if (errorMessage) {
113
+          this.$message.error(errorMessage)
114
+
115
+          this.disabled = false
116
+        } else {
117
+          getCaptcha({ phone: this.regisiterForm.phone })
118
+            .then((e) => {})
119
+            .catch((res) => {
120
+            })
121
+          this.time = 60
122
+          const timer = setInterval(() => {
123
+            this.time-- // 倒计时递减
124
+            this.btnText = `${this.time}s后重新发送`
125
+            if (this.time === 0) {
126
+              this.disabled = false
127
+              this.btnText = '重新发送'
128
+              this.time = this.countDown
129
+              clearInterval(timer)
130
+            }
131
+          }, 1000)
132
+        }
133
+      })
134
+    },
135
+    handleLogin() {
136
+      this.$refs.regisiterForm.validate((valid) => {
137
+        if (valid) {
138
+          this.regisiterForm.password = md5(this.regisiterForm.password)
139
+          // signUp(this.regisiterForm).then((res) => {
140
+          this.$message('修改密码成功')
141
+          this.$router.push({ path: '/login' })
142
+          // })
143
+        } else {
144
+          return false
145
+        }
146
+      })
147
+    }
148
+  }
149
+}
150
+</script>
151
+
152
+<style lang="scss">
153
+/* 修复input 背景不协调 和光标变色 */
154
+/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
155
+
156
+$bg: #283443;
157
+$light_gray: #fff;
158
+$cursor: #fff;
159
+
160
+@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
161
+  .login-container .el-input input {
162
+    color: $cursor;
163
+  }
164
+}
165
+
166
+/* reset element-ui css */
167
+.login-container {
168
+  .el-input {
169
+    display: inline-block;
170
+    height: 47px;
171
+    width: 85%;
172
+
173
+    input {
174
+      background: transparent;
175
+      border: 0px;
176
+      -webkit-appearance: none;
177
+      border-radius: 0px;
178
+      padding: 12px 5px 12px 15px;
179
+      color: $light_gray;
180
+      height: 47px;
181
+      caret-color: $cursor;
182
+
183
+      &:-webkit-autofill {
184
+        box-shadow: 0 0 0px 1000px $bg inset !important;
185
+        -webkit-text-fill-color: $cursor !important;
186
+      }
187
+    }
188
+  }
189
+
190
+  .el-form-item {
191
+    border: 1px solid rgba(255, 255, 255, 0.1);
192
+    background: rgba(0, 0, 0, 0.1);
193
+    border-radius: 5px;
194
+    color: #454545;
195
+  }
196
+}
197
+</style>
198
+
199
+<style lang="scss" scoped>
200
+$bg: #2d3a4b;
201
+$dark_gray: #889aa4;
202
+$light_gray: #eee;
203
+
204
+.login-container {
205
+  min-height: 100%;
206
+  width: 100%;
207
+  background-color: $bg;
208
+  overflow: hidden;
209
+
210
+  .login-form {
211
+    position: relative;
212
+    width: 520px;
213
+    max-width: 100%;
214
+    padding: 160px 35px 0;
215
+    margin: 0 auto;
216
+    overflow: hidden;
217
+  }
218
+
219
+  .tips {
220
+    font-size: 14px;
221
+    color: #fff;
222
+    margin-bottom: 10px;
223
+
224
+    span {
225
+      &:first-of-type {
226
+        margin-right: 16px;
227
+      }
228
+    }
229
+  }
230
+
231
+  .svg-container {
232
+    padding: 6px 5px 6px 15px;
233
+    color: $dark_gray;
234
+    vertical-align: middle;
235
+    width: 30px;
236
+    display: inline-block;
237
+  }
238
+
239
+  .title-container {
240
+    position: relative;
241
+
242
+    .title {
243
+      font-size: 26px;
244
+      color: $light_gray;
245
+      margin: 0px auto 40px auto;
246
+      text-align: center;
247
+      font-weight: bold;
248
+    }
249
+  }
250
+
251
+  .show-pwd {
252
+    position: absolute;
253
+    right: 10px;
254
+    top: 7px;
255
+    font-size: 16px;
256
+    color: $dark_gray;
257
+    cursor: pointer;
258
+    user-select: none;
259
+  }
260
+}
261
+</style>

+ 246
- 0
src/views/login/index.vue Näytä tiedosto

@@ -0,0 +1,246 @@
1
+<template>
2
+  <div class="login-container">
3
+    <el-form
4
+      ref="loginForm"
5
+      :model="loginForm"
6
+      :rules="loginRules"
7
+      class="login-form"
8
+      auto-complete="on"
9
+      label-position="left"
10
+    >
11
+      <div class="title-container">
12
+        <h3 class="title">答题管理系统</h3>
13
+      </div>
14
+
15
+      <el-form-item prop="userName">
16
+        <span class="svg-container">
17
+          <svg-icon icon-class="user" />
18
+        </span>
19
+        <el-input
20
+          ref="userName"
21
+          v-model="loginForm.userName"
22
+          placeholder="用户名"
23
+          name="userName"
24
+          type="text"
25
+          tabindex="1"
26
+          auto-complete="on"
27
+        />
28
+      </el-form-item>
29
+
30
+      <el-form-item prop="password">
31
+        <span class="svg-container">
32
+          <svg-icon icon-class="password" />
33
+        </span>
34
+        <el-input
35
+          :key="passwordType"
36
+          ref="password"
37
+          v-model="loginForm.password"
38
+          :type="passwordType"
39
+          placeholder="密码"
40
+          name="password"
41
+          tabindex="2"
42
+          auto-complete="on"
43
+          @keyup.enter.native="handleLogin"
44
+        />
45
+        <span class="show-pwd" @click="showPwd">
46
+          <svg-icon
47
+            :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'"
48
+          />
49
+        </span>
50
+      </el-form-item>
51
+
52
+      <el-button
53
+        :loading="loading"
54
+        type="primary"
55
+        style="width: 100%; margin-bottom: 30px"
56
+        @click.native.prevent="handleLogin"
57
+      >登录</el-button>
58
+      <el-link :underline="false" style="margin:0" type="primary">
59
+        <router-link
60
+          :to="{name: 'register'}"
61
+        >注册账号</router-link>
62
+      </el-link>
63
+      <el-link :underline="false" style="float:right" type="primary">
64
+        <router-link
65
+          :to="{name: 'forgotPassword'}"
66
+        >忘记密码</router-link>
67
+      </el-link>
68
+    </el-form>
69
+  </div>
70
+</template>
71
+<script>
72
+import md5 from 'js-md5'
73
+export default {
74
+  name: 'Login',
75
+  data() {
76
+    return {
77
+      loginForm: {
78
+        userName: '',
79
+        password: ''
80
+      },
81
+      loginRules: {
82
+        userName: [
83
+          { required: true, message: '请输入用户名', trigger: 'change' }
84
+        ],
85
+        password: [
86
+          { required: true, trigger: 'change', message: '请输入密码' }
87
+        ]
88
+      },
89
+      loading: false,
90
+      passwordType: 'password',
91
+      redirect: undefined
92
+    }
93
+  },
94
+  watch: {
95
+    $route: {
96
+      handler: function(route) {
97
+        this.redirect = route.query && route.query.redirect
98
+      },
99
+      immediate: true
100
+    }
101
+  },
102
+  methods: {
103
+    showPwd() {
104
+      if (this.passwordType === 'password') {
105
+        this.passwordType = ''
106
+      } else {
107
+        this.passwordType = 'password'
108
+      }
109
+      this.$nextTick(() => {
110
+        this.$refs.password.focus()
111
+      })
112
+    },
113
+    handleLogin() {
114
+      this.$refs.loginForm.validate((valid) => {
115
+        if (valid) {
116
+          this.loading = true
117
+          this.loginForm.password = md5(this.loginForm.password)
118
+          this.$store
119
+            .dispatch('user/login', this.loginForm)
120
+            .then(() => {
121
+              this.$router.push({ path: '/' })
122
+              this.loading = false
123
+            })
124
+            .catch(() => {
125
+              this.loading = false
126
+            })
127
+        } else {
128
+          console.log('error submit!!')
129
+          return false
130
+        }
131
+      })
132
+    }
133
+  }
134
+}
135
+</script>
136
+
137
+<style lang="scss">
138
+/* 修复input 背景不协调 和光标变色 */
139
+/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
140
+
141
+$bg: #283443;
142
+$light_gray: #fff;
143
+$cursor: #fff;
144
+
145
+@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
146
+  .login-container .el-input input {
147
+    color: $cursor;
148
+  }
149
+}
150
+
151
+/* reset element-ui css */
152
+.login-container {
153
+  .el-input {
154
+    display: inline-block;
155
+    height: 47px;
156
+    width: 85%;
157
+
158
+    input {
159
+      background: transparent;
160
+      border: 0px;
161
+      -webkit-appearance: none;
162
+      border-radius: 0px;
163
+      padding: 12px 5px 12px 15px;
164
+      color: $light_gray;
165
+      height: 47px;
166
+      caret-color: $cursor;
167
+
168
+      &:-webkit-autofill {
169
+        box-shadow: 0 0 0px 1000px $bg inset !important;
170
+        -webkit-text-fill-color: $cursor !important;
171
+      }
172
+    }
173
+  }
174
+
175
+  .el-form-item {
176
+    border: 1px solid rgba(255, 255, 255, 0.1);
177
+    background: rgba(0, 0, 0, 0.1);
178
+    border-radius: 5px;
179
+    color: #454545;
180
+  }
181
+}
182
+</style>
183
+
184
+<style lang="scss" scoped>
185
+$bg: #2d3a4b;
186
+$dark_gray: #889aa4;
187
+$light_gray: #eee;
188
+
189
+.login-container {
190
+  min-height: 100%;
191
+  width: 100%;
192
+  background-color: $bg;
193
+  overflow: hidden;
194
+
195
+  .login-form {
196
+    position: relative;
197
+    width: 520px;
198
+    max-width: 100%;
199
+    padding: 160px 35px 0;
200
+    margin: 0 auto;
201
+    overflow: hidden;
202
+  }
203
+
204
+  .tips {
205
+    font-size: 14px;
206
+    color: #fff;
207
+    margin-bottom: 10px;
208
+
209
+    span {
210
+      &:first-of-type {
211
+        margin-right: 16px;
212
+      }
213
+    }
214
+  }
215
+
216
+  .svg-container {
217
+    padding: 6px 5px 6px 15px;
218
+    color: $dark_gray;
219
+    vertical-align: middle;
220
+    width: 30px;
221
+    display: inline-block;
222
+  }
223
+
224
+  .title-container {
225
+    position: relative;
226
+
227
+    .title {
228
+      font-size: 26px;
229
+      color: $light_gray;
230
+      margin: 0px auto 40px auto;
231
+      text-align: center;
232
+      font-weight: bold;
233
+    }
234
+  }
235
+
236
+  .show-pwd {
237
+    position: absolute;
238
+    right: 10px;
239
+    top: 7px;
240
+    font-size: 16px;
241
+    color: $dark_gray;
242
+    cursor: pointer;
243
+    user-select: none;
244
+  }
245
+}
246
+</style>

+ 329
- 0
src/views/login/register.vue Näytä tiedosto

@@ -0,0 +1,329 @@
1
+<template>
2
+  <div class="login-container">
3
+    <el-form
4
+      ref="regisiterForm"
5
+      :model="regisiterForm"
6
+      :rules="regisiterRules"
7
+      class="login-form"
8
+      auto-complete="on"
9
+      label-position="left"
10
+    >
11
+      <div class="title-container">
12
+        <h3 class="title">注册账号</h3>
13
+      </div>
14
+
15
+      <el-form-item prop="name">
16
+        <span class="svg-container">
17
+          <svg-icon icon-class="user" />
18
+        </span>
19
+        <el-input
20
+          ref="name"
21
+          v-model="regisiterForm.name"
22
+          placeholder="用户名"
23
+          name="name"
24
+          type="text"
25
+          tabindex="1"
26
+        />
27
+      </el-form-item>
28
+
29
+      <el-form-item prop="password">
30
+        <span class="svg-container">
31
+          <svg-icon icon-class="password" />
32
+        </span>
33
+        <el-input
34
+          :key="passwordType"
35
+          ref="password"
36
+          v-model="regisiterForm.password"
37
+          :type="passwordType"
38
+          placeholder="密码"
39
+          name="password"
40
+          tabindex="2"
41
+        />
42
+        <span class="show-pwd" @click="showPwd">
43
+          <svg-icon
44
+            :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'"
45
+          />
46
+        </span>
47
+      </el-form-item>
48
+      <el-form-item prop="password2">
49
+        <span class="svg-container">
50
+          <svg-icon icon-class="password" />
51
+        </span>
52
+        <el-input
53
+          ref="password2"
54
+          v-model="regisiterForm.password2"
55
+          type="text"
56
+          placeholder="确认密码"
57
+          name="password2"
58
+          tabindex="3"
59
+        />
60
+      </el-form-item>
61
+      <el-form-item prop="phone">
62
+        <span class="svg-container">
63
+          <svg-icon icon-class="password" />
64
+        </span>
65
+        <el-input
66
+          ref="phone"
67
+          v-model="regisiterForm.phone"
68
+          type="tel"
69
+          placeholder="手机号"
70
+          name="phone"
71
+          tabindex="4"
72
+        />
73
+      </el-form-item>
74
+
75
+      <el-form-item prop="captcha" style="width:65%">
76
+        <span class="svg-container">
77
+          <svg-icon icon-class="password" />
78
+        </span>
79
+        <el-input
80
+          ref="captcha"
81
+          v-model="regisiterForm.captcha"
82
+          type="text"
83
+          placeholder="验证码"
84
+          tabindex="5"
85
+          name="captcha"
86
+          @keyup.enter.native="handleLogin"
87
+        />
88
+        <el-button style="position: absolute;right: -150px;top: 6px;" :disabled="disabled" @click="sendCode">{{ btnText }}</el-button>
89
+      </el-form-item>
90
+
91
+      <el-button
92
+        :loading="loading"
93
+        type="primary"
94
+        style="width: 100%; margin-bottom: 30px"
95
+        @click.native.prevent="handleLogin"
96
+      >注册</el-button>
97
+      <el-link :underline="false" style="margin:0" type="primary">
98
+        <router-link
99
+          :to="{path: '/login'}"
100
+        >登录系统</router-link>
101
+      </el-link>
102
+      <el-link :underline="false" style="float:right" type="primary">
103
+        <router-link
104
+          :to="{name: 'forgotPassword'}"
105
+        >忘记密码</router-link>
106
+      </el-link>
107
+    </el-form>
108
+  </div>
109
+</template>
110
+<script>
111
+import { getCaptcha, signUp } from '@/api/signup'
112
+import md5 from 'js-md5'
113
+export default {
114
+  name: 'Login',
115
+  data() {
116
+    const validatePhone = (rule, value, callback) => {
117
+      if (!value) {
118
+        return callback(new Error('手机号不能为空'))
119
+      } else {
120
+        const reg = /^1[3|4|5|7|8][0-9]\d{8}$/
121
+        if (reg.test(value)) {
122
+          callback()
123
+        } else {
124
+          return callback(new Error('请输入正确的手机号'))
125
+        }
126
+      }
127
+    }
128
+    return {
129
+      regisiterForm: {
130
+        name: undefined,
131
+        password: undefined,
132
+        phone: undefined,
133
+        captcha: '619400',
134
+        password2: undefined
135
+      },
136
+      regisiterRules: {
137
+        name: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
138
+        password: [
139
+          { required: true, trigger: 'change', message: '请输入密码' }
140
+        ],
141
+        password2: [
142
+          { required: true, trigger: 'change', message: '请再次输入密码' }
143
+        ],
144
+        phone: [
145
+          { required: true, trigger: 'change', validator: validatePhone }
146
+        ],
147
+        captcha: [
148
+          { required: true, trigger: 'change', message: '请输入验证码' }
149
+        ]
150
+      },
151
+      disabled: false,
152
+      btnText: '发送验证码',
153
+      time: 0,
154
+      loading: false,
155
+      passwordType: 'password'
156
+    }
157
+  },
158
+  methods: {
159
+    sendCode() {
160
+      this.disabled = true
161
+
162
+      this.$refs.regisiterForm.validateField('phone', (errorMessage) => {
163
+        if (errorMessage) {
164
+          this.$message.error(errorMessage)
165
+
166
+          this.disabled = false
167
+        } else {
168
+          getCaptcha({ phone: this.regisiterForm.phone })
169
+            .then((e) => {})
170
+            .catch((res) => {
171
+              // console.log(res)
172
+            })
173
+          this.time = 60
174
+          const timer = setInterval(() => {
175
+            this.time-- // 倒计时递减
176
+            this.btnText = `${this.time}s后重新发送`
177
+            if (this.time === 0) {
178
+              this.disabled = false
179
+              this.btnText = '重新发送'
180
+              this.time = this.countDown
181
+              clearInterval(timer)
182
+            }
183
+          }, 1000)
184
+        }
185
+      })
186
+    },
187
+    showPwd() {
188
+      if (this.passwordType === 'password') {
189
+        this.passwordType = ''
190
+      } else {
191
+        this.passwordType = 'password'
192
+      }
193
+      this.$nextTick(() => {
194
+        this.$refs.password.focus()
195
+      })
196
+    },
197
+    handleLogin() {
198
+      this.$refs.regisiterForm.validate((valid) => {
199
+        if (valid) {
200
+          if (this.regisiterForm.password !== this.regisiterForm.password2) {
201
+            this.$message('密码输入不一致请重新输入')
202
+            this.$refs.password2.focus()
203
+            return false
204
+          } else {
205
+            this.regisiterForm.password = md5(this.regisiterForm.password)
206
+            signUp(this.regisiterForm).then((res) => {
207
+              this.$message('注册成功')
208
+              this.$router.push({ path: '/login' })
209
+            })
210
+          }
211
+        } else {
212
+          return false
213
+        }
214
+      })
215
+    }
216
+  }
217
+}
218
+</script>
219
+
220
+<style lang="scss">
221
+/* 修复input 背景不协调 和光标变色 */
222
+/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
223
+
224
+$bg: #283443;
225
+$light_gray: #fff;
226
+$cursor: #fff;
227
+
228
+@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
229
+  .login-container .el-input input {
230
+    color: $cursor;
231
+  }
232
+}
233
+
234
+/* reset element-ui css */
235
+.login-container {
236
+  .el-input {
237
+    display: inline-block;
238
+    height: 47px;
239
+    width: 85%;
240
+
241
+    input {
242
+      background: transparent;
243
+      border: 0px;
244
+      -webkit-appearance: none;
245
+      border-radius: 0px;
246
+      padding: 12px 5px 12px 15px;
247
+      color: $light_gray;
248
+      height: 47px;
249
+      caret-color: $cursor;
250
+
251
+      &:-webkit-autofill {
252
+        box-shadow: 0 0 0px 1000px $bg inset !important;
253
+        -webkit-text-fill-color: $cursor !important;
254
+      }
255
+    }
256
+  }
257
+
258
+  .el-form-item {
259
+    border: 1px solid rgba(255, 255, 255, 0.1);
260
+    background: rgba(0, 0, 0, 0.1);
261
+    border-radius: 5px;
262
+    color: #454545;
263
+  }
264
+}
265
+</style>
266
+
267
+<style lang="scss" scoped>
268
+$bg: #2d3a4b;
269
+$dark_gray: #889aa4;
270
+$light_gray: #eee;
271
+
272
+.login-container {
273
+  min-height: 100%;
274
+  width: 100%;
275
+  background-color: $bg;
276
+  overflow: hidden;
277
+
278
+  .login-form {
279
+    position: relative;
280
+    width: 520px;
281
+    max-width: 100%;
282
+    padding: 160px 35px 0;
283
+    margin: 0 auto;
284
+    overflow: hidden;
285
+  }
286
+
287
+  .tips {
288
+    font-size: 14px;
289
+    color: #fff;
290
+    margin-bottom: 10px;
291
+
292
+    span {
293
+      &:first-of-type {
294
+        margin-right: 16px;
295
+      }
296
+    }
297
+  }
298
+
299
+  .svg-container {
300
+    padding: 6px 5px 6px 15px;
301
+    color: $dark_gray;
302
+    vertical-align: middle;
303
+    width: 30px;
304
+    display: inline-block;
305
+  }
306
+
307
+  .title-container {
308
+    position: relative;
309
+
310
+    .title {
311
+      font-size: 26px;
312
+      color: $light_gray;
313
+      margin: 0px auto 40px auto;
314
+      text-align: center;
315
+      font-weight: bold;
316
+    }
317
+  }
318
+
319
+  .show-pwd {
320
+    position: absolute;
321
+    right: 10px;
322
+    top: 7px;
323
+    font-size: 16px;
324
+    color: $dark_gray;
325
+    cursor: pointer;
326
+    user-select: none;
327
+  }
328
+}
329
+</style>

+ 5
- 0
tests/unit/.eslintrc.js Näytä tiedosto

@@ -0,0 +1,5 @@
1
+module.exports = {
2
+  env: {
3
+    jest: true
4
+  }
5
+}

+ 70
- 0
tests/unit/components/Breadcrumb.spec.js Näytä tiedosto

@@ -0,0 +1,70 @@
1
+import { mount, createLocalVue } from '@vue/test-utils'
2
+import VueRouter from 'vue-router'
3
+import ElementUI from 'element-ui'
4
+import Breadcrumb from '@/components/Breadcrumb/index.vue'
5
+
6
+const localVue = createLocalVue()
7
+localVue.use(VueRouter)
8
+localVue.use(ElementUI)
9
+
10
+const routes = [
11
+  {
12
+    path: '/',
13
+    name: 'home',
14
+    children: [{
15
+      path: 'dashboard',
16
+      name: 'dashboard'
17
+    }]
18
+  },  
19
+]
20
+
21
+const router = new VueRouter({
22
+  routes
23
+})
24
+
25
+describe('Breadcrumb.vue', () => {
26
+  const wrapper = mount(Breadcrumb, {
27
+    localVue,
28
+    router
29
+  })
30
+  it('dashboard', () => {
31
+    router.push('/dashboard')
32
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
33
+    expect(len).toBe(1)
34
+  })
35
+  it('normal route', () => {
36
+    router.push('/menu/menu1')
37
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
38
+    expect(len).toBe(2)
39
+  })
40
+  it('nested route', () => {
41
+    router.push('/menu/menu1/menu1-2/menu1-2-1')
42
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
43
+    expect(len).toBe(4)
44
+  })
45
+  it('no meta.title', () => {
46
+    router.push('/menu/menu1/menu1-2/menu1-2-2')
47
+    const len = wrapper.findAll('.el-breadcrumb__inner').length
48
+    expect(len).toBe(3)
49
+  })
50
+  // it('click link', () => {
51
+  //   router.push('/menu/menu1/menu1-2/menu1-2-2')
52
+  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
53
+  //   const second = breadcrumbArray.at(1)
54
+  //   console.log(breadcrumbArray)
55
+  //   const href = second.find('a').attributes().href
56
+  //   expect(href).toBe('#/menu/menu1')
57
+  // })
58
+  // it('noRedirect', () => {
59
+  //   router.push('/menu/menu1/menu1-2/menu1-2-1')
60
+  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
61
+  //   const redirectBreadcrumb = breadcrumbArray.at(2)
62
+  //   expect(redirectBreadcrumb.contains('a')).toBe(false)
63
+  // })
64
+  it('last breadcrumb', () => {
65
+    router.push('/menu/menu1/menu1-2/menu1-2-1')
66
+    const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
67
+    const redirectBreadcrumb = breadcrumbArray.at(3)
68
+    expect(redirectBreadcrumb.contains('a')).toBe(false)
69
+  })
70
+})

+ 18
- 0
tests/unit/components/Hamburger.spec.js Näytä tiedosto

@@ -0,0 +1,18 @@
1
+import { shallowMount } from '@vue/test-utils'
2
+import Hamburger from '@/components/Hamburger/index.vue'
3
+describe('Hamburger.vue', () => {
4
+  it('toggle click', () => {
5
+    const wrapper = shallowMount(Hamburger)
6
+    const mockFn = jest.fn()
7
+    wrapper.vm.$on('toggleClick', mockFn)
8
+    wrapper.find('.hamburger').trigger('click')
9
+    expect(mockFn).toBeCalled()
10
+  })
11
+  it('prop isActive', () => {
12
+    const wrapper = shallowMount(Hamburger)
13
+    wrapper.setProps({ isActive: true })
14
+    expect(wrapper.contains('.is-active')).toBe(true)
15
+    wrapper.setProps({ isActive: false })
16
+    expect(wrapper.contains('.is-active')).toBe(false)
17
+  })
18
+})

+ 22
- 0
tests/unit/components/SvgIcon.spec.js Näytä tiedosto

@@ -0,0 +1,22 @@
1
+import { shallowMount } from '@vue/test-utils'
2
+import SvgIcon from '@/components/SvgIcon/index.vue'
3
+describe('SvgIcon.vue', () => {
4
+  it('iconClass', () => {
5
+    const wrapper = shallowMount(SvgIcon, {
6
+      propsData: {
7
+        iconClass: 'test'
8
+      }
9
+    })
10
+    expect(wrapper.find('use').attributes().href).toBe('#icon-test')
11
+  })
12
+  it('className', () => {
13
+    const wrapper = shallowMount(SvgIcon, {
14
+      propsData: {
15
+        iconClass: 'test'
16
+      }
17
+    })
18
+    expect(wrapper.classes().length).toBe(1)
19
+    wrapper.setProps({ className: 'test' })
20
+    expect(wrapper.classes().includes('test')).toBe(true)
21
+  })
22
+})

+ 30
- 0
tests/unit/utils/formatTime.spec.js Näytä tiedosto

@@ -0,0 +1,30 @@
1
+import { formatTime } from '@/utils/index.js'
2
+
3
+describe('Utils:formatTime', () => {
4
+  const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5
+  const retrofit = 5 * 1000
6
+
7
+  it('ten digits timestamp', () => {
8
+    expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')
9
+  })
10
+  it('test now', () => {
11
+    expect(formatTime(+new Date() - 1)).toBe('刚刚')
12
+  })
13
+  it('less two minute', () => {
14
+    expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')
15
+  })
16
+  it('less two hour', () => {
17
+    expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')
18
+  })
19
+  it('less one day', () => {
20
+    expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')
21
+  })
22
+  it('more than one day', () => {
23
+    expect(formatTime(d)).toBe('7月13日17时54分')
24
+  })
25
+  it('format', () => {
26
+    expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
27
+    expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
28
+    expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
29
+  })
30
+})

+ 14
- 0
tests/unit/utils/param2Obj.spec.js Näytä tiedosto

@@ -0,0 +1,14 @@
1
+import { param2Obj } from '@/utils/index.js'
2
+describe('Utils:param2Obj', () => {
3
+  const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95'
4
+
5
+  it('param2Obj test', () => {
6
+    expect(param2Obj(url)).toEqual({
7
+      name: 'bill',
8
+      age: '29',
9
+      sex: '1',
10
+      field: window.btoa('test'),
11
+      key: '测试'
12
+    })
13
+  })
14
+})

+ 35
- 0
tests/unit/utils/parseTime.spec.js Näytä tiedosto

@@ -0,0 +1,35 @@
1
+import { parseTime } from '@/utils/index.js'
2
+
3
+describe('Utils:parseTime', () => {
4
+  const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5
+  it('timestamp', () => {
6
+    expect(parseTime(d)).toBe('2018-07-13 17:54:01')
7
+  })
8
+  it('timestamp string', () => {
9
+    expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01')
10
+  })
11
+  it('ten digits timestamp', () => {
12
+    expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
13
+  })
14
+  it('new Date', () => {
15
+    expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
16
+  })
17
+  it('format', () => {
18
+    expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
19
+    expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
20
+    expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
21
+  })
22
+  it('get the day of the week', () => {
23
+    expect(parseTime(d, '{a}')).toBe('五') // 星期五
24
+  })
25
+  it('get the day of the week', () => {
26
+    expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
27
+  })
28
+  it('empty argument', () => {
29
+    expect(parseTime()).toBeNull()
30
+  })
31
+
32
+  it('null', () => {
33
+    expect(parseTime(null)).toBeNull()
34
+  })
35
+})

+ 17
- 0
tests/unit/utils/validate.spec.js Näytä tiedosto

@@ -0,0 +1,17 @@
1
+import { validUsername, isExternal } from '@/utils/validate.js'
2
+
3
+describe('Utils:validate', () => {
4
+  it('validUsername', () => {
5
+    expect(validUsername('admin')).toBe(true)
6
+    expect(validUsername('editor')).toBe(true)
7
+    expect(validUsername('xxxx')).toBe(false)
8
+  })
9
+  it('isExternal', () => {
10
+    expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
11
+    expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
12
+    expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false)
13
+    expect(isExternal('/dashboard')).toBe(false)
14
+    expect(isExternal('./dashboard')).toBe(false)
15
+    expect(isExternal('dashboard')).toBe(false)
16
+  })
17
+})

+ 0
- 13
yarn.lock Näytä tiedosto

@@ -4749,7 +4749,6 @@ file-loader@^4.2.0:
4749 4749
     loader-utils "^1.2.3"
4750 4750
     schema-utils "^2.5.0"
4751 4751
 
4752
-
4753 4752
 file-uri-to-path@1.0.0:
4754 4753
   version "1.0.0"
4755 4754
   resolved "https://registry.npmmirror.com/file-uri-to-path/download/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -9093,11 +9092,6 @@ raw-body@2.4.2, raw-body@^2.2.0:
9093 9092
     iconv-lite "0.4.24"
9094 9093
     unpipe "1.0.0"
9095 9094
 
9096
-raw-loader@~0.5.1:
9097
-  version "0.5.1"
9098
-  resolved "https://registry.npmmirror.com/raw-loader/download/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
9099
-  integrity sha1-DD0L6u2KAclm2Xh793goElKpeao=
9100
-
9101 9095
 react-is@^16.8.4:
9102 9096
   version "16.13.1"
9103 9097
   resolved "https://registry.npmmirror.com/react-is/download/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -9607,13 +9601,6 @@ script-ext-html-webpack-plugin@2.1.3:
9607 9601
   dependencies:
9608 9602
     debug "^4.1.0"
9609 9603
 
9610
-script-loader@^0.7.2:
9611
-  version "0.7.2"
9612
-  resolved "https://registry.npmmirror.com/script-loader/download/script-loader-0.7.2.tgz#2016db6f86f25f5cf56da38915d83378bb166ba7"
9613
-  integrity sha1-IBbbb4byX1z1baOJFdgzeLsWa6c=
9614
-  dependencies:
9615
-    raw-loader "~0.5.1"
9616
-
9617 9604
 sdk-base@^2.0.1:
9618 9605
   version "2.0.1"
9619 9606
   resolved "https://registry.npmmirror.com/sdk-base/download/sdk-base-2.0.1.tgz#ba40289e8bdf272ed11dd9ea97eaf98e036d24c6"