wangyang 5 роки тому
батько
коміт
153c785875

+ 68 - 6
src/api/user.js

@@ -1,24 +1,86 @@
 import request from '@/utils/request'
 
+// 登录
 export function login(data) {
   return request({
-    url: '/vue-admin-template/user/login',
+    url: '/api/login',
     method: 'post',
     data
   })
 }
 
-export function getInfo(token) {
+export function getInfo() {
   return request({
-    url: '/vue-admin-template/user/info',
-    method: 'get',
-    params: { token }
+    url: '/api/users/info',
+    method: 'get'
   })
 }
 
 export function logout() {
   return request({
-    url: '/vue-admin-template/user/logout',
+    url: '/api/logout',
+    method: 'post'
+  })
+}
+
+// 更新密码
+export function changePassword(id, data) {
+  return request({
+    url: `/api/users/${id}/change-password`,
+    method: 'post',
+    data
+  })
+}
+
+// 获取用户列表
+export function getUsers(params) {
+  return request({
+    url: '/api/admin/users',
+    method: 'get',
+    params
+  })
+}
+
+// 创建用户
+export function createUser(data) {
+  return request({
+    url: `/api/admin/users`,
+    method: 'post',
+    data
+  })
+}
+
+// 修改用户信息
+export function updateUser(id, data) {
+  return request({
+    url: `/api/admin/users/${id}/info`,
+    method: 'post',
+    data
+  })
+}
+
+// 重置用户密码
+export function resetUserPassword(id, password) {
+  return request({
+    url: `/api/admin/users/${id}/password`,
+    method: 'post',
+    data: { password }
+  })
+}
+
+// 冻结或解冻用户
+export function changeUserFrozen(id) {
+  return request({
+    url: `/api/admin/users/${id}/frozen`,
     method: 'post'
   })
 }
+
+// 修改用户个人信息
+export function updateSelfUserInfo(data) {
+  return request({
+    url: `/api/users/info`,
+    method: 'post',
+    data
+  })
+}

+ 1 - 1
src/components/Breadcrumb/index.vue

@@ -33,7 +33,7 @@ export default {
       const first = matched[0]
 
       if (!this.isDashboard(first)) {
-        matched = [{ path: '/food', meta: { title: '食物列表' }}].concat(matched)
+        matched = [{ path: '/food', meta: { title: '首页' }}].concat(matched)
       }
 
       this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)

+ 11 - 0
src/components/Upload/SingleImage.vue

@@ -4,10 +4,12 @@
       :data="dataObj"
       :multiple="false"
       :show-file-list="false"
+      :headers="uploadHeaders"
       :on-success="handleImageSuccess"
       class="image-uploader"
       drag
       :action="uploadHost"
+      v-if="imageUrl.length <= 0"
     >
       <i class="el-icon-upload" />
       <div class="el-upload__text">
@@ -26,6 +28,8 @@
 </template>
 
 <script>
+  import store from '@/store'
+
   export default {
     name: 'SingleImageUpload',
     props: {
@@ -48,16 +52,23 @@
       imageUrl() {
         return this.value
       },
+      uploadHeaders() {
+        return {
+          'Authorization': store.getters.token
+        }
+      }
     },
     methods: {
       rmImage() {
         this.emitInput('')
+        this.$emit("success", "")
       },
       emitInput(val) {
         this.$emit('input', val)
       },
       handleImageSuccess(response) {
         this.emitInput(process.env.VUE_APP_BASE_API + '/api/files/view-file?path=' + response.url)
+        this.$emit("success", process.env.VUE_APP_BASE_API + '/api/files/view-file?path=' + response.url)
       }
     }
   }

+ 152 - 90
src/layout/components/Navbar.vue

@@ -13,127 +13,189 @@
         <el-dropdown-menu slot="dropdown" class="user-dropdown">
           <router-link to="/">
             <el-dropdown-item>
-              Home
+              首页
             </el-dropdown-item>
           </router-link>
-          <a target="_blank" href="https://github.com/PanJiaChen/vue-admin-template/">
-            <el-dropdown-item>Github</el-dropdown-item>
-          </a>
-          <a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
-            <el-dropdown-item>Docs</el-dropdown-item>
-          </a>
+          <el-dropdown-item divided @click.native="changePassword">
+            <span style="display:block;">修改密码</span>
+          </el-dropdown-item>
           <el-dropdown-item divided @click.native="logout">
-            <span style="display:block;">Log Out</span>
+            <span style="display:block;">登出</span>
           </el-dropdown-item>
         </el-dropdown-menu>
       </el-dropdown>
     </div>
+
+    <el-dialog title="修改密码" :visible.sync="dialogFormVisible">
+      <el-form
+        ref="dataForm"
+        :rules="rules"
+        :model="params"
+        label-position="left"
+        label-width="120px"
+        style="width: 400px; margin-left:50px;"
+      >
+        <el-form-item label="原密码" prop="password">
+          <el-input v-model="params.password" placeholder="请输入原密码" show-password />
+        </el-form-item>
+        <el-form-item label="新密码" prop="newPassword">
+          <el-input v-model="params.newPassword" placeholder="请输入新密码" show-password />
+        </el-form-item>
+        <el-form-item label="新密码确认" prop="newPassword">
+          <el-input v-model="params.newPasswordConfirm" placeholder="请再次输入新密码" show-password />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submit()">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { mapGetters } from 'vuex'
-import Breadcrumb from '@/components/Breadcrumb'
-import Hamburger from '@/components/Hamburger'
-
-export default {
-  components: {
-    Breadcrumb,
-    Hamburger
-  },
-  computed: {
-    ...mapGetters([
-      'sidebar',
-      'avatar'
-    ])
-  },
-  methods: {
-    toggleSideBar() {
-      this.$store.dispatch('app/toggleSideBar')
+  import { mapGetters } from 'vuex'
+  import Breadcrumb from '@/components/Breadcrumb'
+  import Hamburger from '@/components/Hamburger'
+  import { changePassword } from '@/api/user'
+
+  export default {
+    components: {
+      Breadcrumb,
+      Hamburger
+    },
+    computed: {
+      ...mapGetters([
+        'sidebar',
+        'avatar',
+        'userId'
+      ])
+    },
+    data() {
+      return {
+        dialogFormVisible: false,
+        params: {},
+        rules: {
+          password: [{ required: true, message: '不允许为空', trigger: 'blur' }],
+          newPassword: [{ required: true, message: '不允许为空', trigger: 'blur' }],
+          newPasswordConfirm: [{ required: true, message: '不允许为空', trigger: 'blur' }]
+        }
+      }
     },
-    async logout() {
-      await this.$store.dispatch('user/logout')
-      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+    methods: {
+      toggleSideBar() {
+        this.$store.dispatch('app/toggleSideBar')
+      },
+      async logout() {
+        await this.$store.dispatch('user/logout')
+        this.$router.push(`/login?redirect=${this.$route.fullPath}`)
+      },
+      changePassword() {
+        this.dialogFormVisible = true
+        this.params = {}
+      },
+      submit() {
+        this.$refs['dataForm'].validate((valid) => {
+          if (valid) {
+            if (this.params.newPassword !== this.params.newPasswordConfirm) {
+              this.$notify({ title: '失败', message: '两次输入密码不一致', type: 'error', duration: 2000 })
+            } else {
+              changePassword(this.userId, this.params).then(res => {
+                this.$notify({ title: '成功', message: '修改密码成功', type: 'success', duration: 2000 })
+                this.dialogFormVisible = false
+              }).catch(error => {
+                this.$notify({ title: '失败', message: error.response.message, type: 'error', duration: 2000 })
+                this.dialogFormVisible = false
+              })
+            }
+          } else {
+            this.$notify({ title: '失败', message: '有必填字段为空', type: 'error', duration: 2000 })
+          }
+        })
+      }
     }
   }
-}
 </script>
 
 <style lang="scss" scoped>
-.navbar {
-  height: 50px;
-  overflow: hidden;
-  position: relative;
-  background: #fff;
-  box-shadow: 0 1px 4px rgba(0,21,41,.08);
-
-  .hamburger-container {
-    line-height: 46px;
-    height: 100%;
-    float: left;
-    cursor: pointer;
-    transition: background .3s;
-    -webkit-tap-highlight-color:transparent;
-
-    &:hover {
-      background: rgba(0, 0, 0, .025)
-    }
-  }
-
-  .breadcrumb-container {
-    float: left;
-  }
+  .navbar {
+    height: 50px;
+    overflow: hidden;
+    position: relative;
+    background: #fff;
+    box-shadow: 0 1px 4px rgba(0,21,41,.08);
+
+    .hamburger-container {
+      line-height: 46px;
+      height: 100%;
+      float: left;
+      cursor: pointer;
+      transition: background .3s;
+      -webkit-tap-highlight-color:transparent;
 
-  .right-menu {
-    float: right;
-    height: 100%;
-    line-height: 50px;
+      &:hover {
+        background: rgba(0, 0, 0, .025)
+      }
+    }
 
-    &:focus {
-      outline: none;
+    .breadcrumb-container {
+      float: left;
     }
 
-    .right-menu-item {
-      display: inline-block;
-      padding: 0 8px;
+    .right-menu {
+      float: right;
       height: 100%;
-      font-size: 18px;
-      color: #5a5e66;
-      vertical-align: text-bottom;
+      line-height: 50px;
 
-      &.hover-effect {
-        cursor: pointer;
-        transition: background .3s;
-
-        &:hover {
-          background: rgba(0, 0, 0, .025)
-        }
+      &:focus {
+        outline: none;
       }
-    }
-
-    .avatar-container {
-      margin-right: 30px;
 
-      .avatar-wrapper {
-        margin-top: 5px;
-        position: relative;
+      .right-menu-item {
+        display: inline-block;
+        padding: 0 8px;
+        height: 100%;
+        font-size: 18px;
+        color: #5a5e66;
+        vertical-align: text-bottom;
 
-        .user-avatar {
+        &.hover-effect {
           cursor: pointer;
-          width: 40px;
-          height: 40px;
-          border-radius: 10px;
+          transition: background .3s;
+
+          &:hover {
+            background: rgba(0, 0, 0, .025)
+          }
         }
+      }
 
-        .el-icon-caret-bottom {
-          cursor: pointer;
-          position: absolute;
-          right: -20px;
-          top: 25px;
-          font-size: 12px;
+      .avatar-container {
+        margin-right: 30px;
+
+        .avatar-wrapper {
+          margin-top: 5px;
+          position: relative;
+
+          .user-avatar {
+            cursor: pointer;
+            width: 40px;
+            height: 40px;
+            border-radius: 10px;
+          }
+
+          .el-icon-caret-bottom {
+            cursor: pointer;
+            position: absolute;
+            right: -20px;
+            top: 25px;
+            font-size: 12px;
+          }
         }
       }
     }
   }
-}
 </style>

+ 18 - 10
src/permission.js

@@ -3,7 +3,7 @@ import store from './store'
 import { Message } from 'element-ui'
 import NProgress from 'nprogress' // progress bar
 import 'nprogress/nprogress.css' // progress bar style
-// import { getToken } from '@/utils/auth' // get token from cookie
+import { getToken } from '@/utils/auth' // get token from cookie
 import getPageTitle from '@/utils/get-page-title'
 
 NProgress.configure({ showSpinner: false }) // NProgress Configuration
@@ -17,12 +17,8 @@ router.beforeEach(async(to, from, next) => {
   // set page title
   document.title = getPageTitle(to.meta.title)
 
-  // 初始化 单位数据
-  await store.dispatch('unit/getUtils')
-
   // determine whether the user has logged in
-  // const hasToken = getToken()
-  const hasToken = true
+  const hasToken = getToken()
 
   if (hasToken) {
     if (to.path === '/login') {
@@ -36,13 +32,25 @@ router.beforeEach(async(to, from, next) => {
       } else {
         try {
           // get user info
-          await store.dispatch('user/getInfo')
-
-          next()
+          await store.dispatch('user/getInfo').then(() => {
+            store.dispatch('permission/generateRoutes', store.getters.isAdmin).then(() => {
+              // 根据roles权限生成可访问的路由表
+              // 动态添加可访问路由表
+              router.options.routes = store.getters.routers
+              router.addRoutes(store.getters.addRouters)
+              const redirect = decodeURIComponent(from.query.redirect || to.path)
+              if (to.path === redirect) {
+                // hack方法 确保addRoutes已完成 ,设置replace:true,以便导航不会留下历史记录
+                next({ ...to, replace: true })
+              } else {
+                // 跳转到目的路由
+                next({ path: redirect })
+              }
+            })
+          })
         } catch (error) {
           // remove token and go to login page to re-login
           await store.dispatch('user/resetToken')
-          Message.error(error || 'Has Error')
           next(`/login?redirect=${to.path}`)
           NProgress.done()
         }

+ 100 - 0
src/router/config.js

@@ -0,0 +1,100 @@
+/* Layout */
+import Layout from '@/layout'
+
+export const asyncRouterMap = [
+  {
+    path: '/',
+    component: Layout,
+    redirect: '/food',
+    children: [
+      {
+        path: 'food',
+        name: '食物列表',
+        component: () => import('@/views/food/index'),
+        meta: { title: '食物列表', icon: 'dashboard' }
+      },
+      {
+        path: 'food/create',
+        name: '食物创建',
+        component: () => import('@/views/food/create'),
+        meta: { title: '食物创建', icon: 'dashboard' },
+        hidden: true
+      },
+      {
+        path: 'food/edit/:id',
+        name: '食物编辑',
+        component: () => import('@/views/food/edit'),
+        meta: { title: '食物编辑', icon: 'dashboard' },
+        hidden: true
+      },
+      {
+        path: 'food/:id/addNutrient',
+        name: '营养素关联',
+        component: () => import('@/views/food/addNutrient'),
+        meta: { title: '营养素关联', icon: 'dashboard' },
+        hidden: true
+      }
+    ]
+  },
+
+  {
+    path: '/nutrient',
+    component: Layout,
+    children: [
+      {
+        path: '',
+        name: '营养素列表',
+        component: () => import('@/views/nutrient/index'),
+        meta: { title: '营养素列表', icon: 'dashboard' }
+      }
+    ]
+  },
+
+  {
+    path: '/unit',
+    component: Layout,
+    children: [
+      {
+        path: '',
+        name: '单位列表',
+        component: () => import('@/views/unit/index'),
+        meta: { title: '单位列表', icon: 'dashboard' }
+      }
+    ]
+  },
+
+  {
+    path: '/user',
+    component: Layout,
+    meta: { admin: true },
+    children: [
+      {
+        path: '',
+        name: '用户列表',
+        component: () => import('@/views/user/index'),
+        meta: { title: '用户列表', icon: 'dashboard' }
+      }
+    ]
+  },
+
+  // 404 page must be placed at the end !!!
+  { path: '*', redirect: '/404', hidden: true }
+]
+
+/**
+ * 基础路由
+ * @type { *[] }
+ */
+export const constantRouterMap = [
+  {
+    path: '/404',
+    hidden: true,
+    component: () => import('@/views/404')
+  },
+  {
+    path: '/login',
+    component: () => import('@/views/login/index'),
+    hidden: true
+  }
+]
+

+ 6 - 93
src/router/index.js

@@ -1,10 +1,9 @@
 import Vue from 'vue'
 import Router from 'vue-router'
 
-Vue.use(Router)
+import { constantRouterMap } from '@/router/config'
 
-/* Layout */
-import Layout from '@/layout'
+Vue.use(Router)
 
 /**
  * Note: sub-menu only appear when route children.length >= 1
@@ -30,96 +29,10 @@ import Layout from '@/layout'
  * a base page that does not have permission requirements
  * all roles can be accessed
  */
-export const constantRoutes = [
-  {
-    path: '/login',
-    component: () => import('@/views/login/index'),
-    hidden: true
-  },
-
-  {
-    path: '/404',
-    component: () => import('@/views/404'),
-    hidden: true
-  },
-
-  {
-    path: '/',
-    component: Layout,
-    redirect: '/food',
-    children: [
-      {
-        path: 'food',
-        name: '食物列表',
-        component: () => import('@/views/food/index'),
-        meta: { title: '食物列表', icon: 'dashboard' }
-      },
-      {
-        path: 'food/create',
-        name: '食物创建',
-        component: () => import('@/views/food/create'),
-        meta: { title: '食物创建', icon: 'dashboard' },
-        hidden: true
-      },
-      {
-        path: 'food/edit/:id',
-        name: '食物编辑',
-        component: () => import('@/views/food/edit'),
-        meta: { title: '食物编辑', icon: 'dashboard' },
-        hidden: true
-      },
-      {
-        path: 'food/:id/addNutrient',
-        name: '营养素关联',
-        component: () => import('@/views/food/addNutrient'),
-        meta: { title: '营养素关联', icon: 'dashboard' },
-        hidden: true
-      }
-    ]
-  },
-
-  {
-    path: '/nutrient',
-    component: Layout,
-    children: [
-      {
-        path: '',
-        name: '营养素列表',
-        component: () => import('@/views/nutrient/index'),
-        meta: { title: '营养素列表', icon: 'dashboard' }
-      }
-    ]
-  },
 
-  {
-    path: '/unit',
-    component: Layout,
-    children: [
-      {
-        path: '',
-        name: '单位列表',
-        component: () => import('@/views/unit/index'),
-        meta: { title: '单位列表', icon: 'dashboard' }
-      }
-    ]
-  },
-
-  // 404 page must be placed at the end !!!
-  { path: '*', redirect: '/404', hidden: true }
-]
-
-const createRouter = () => new Router({
-  // mode: 'history', // require service support
+// export default router
+/* mode: 'history',*/
+export default new Router({
   scrollBehavior: () => ({ y: 0 }),
-  routes: constantRoutes
+  routes: constantRouterMap
 })
-
-const router = createRouter()
-
-// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
-export function resetRouter() {
-  const newRouter = createRouter()
-  router.matcher = newRouter.matcher // reset router
-}
-
-export default router

+ 5 - 1
src/store/getters.js

@@ -2,8 +2,12 @@ const getters = {
   sidebar: state => state.app.sidebar,
   device: state => state.app.device,
   token: state => state.user.token,
+  userId: state => state.user.userId,
   avatar: state => state.user.avatar,
+  isAdmin: state => state.user.isAdmin,
   name: state => state.user.name,
-  units: state => state.unit.units
+  isFrozen: state => state.user.isFrozen,
+  addRouters: state => state.permission.addRouters,
+  routers: state => state.permission.routers
 }
 export default getters

+ 2 - 2
src/store/index.js

@@ -4,7 +4,7 @@ import getters from './getters'
 import app from './modules/app'
 import settings from './modules/settings'
 import user from './modules/user'
-import unit from './modules/unit'
+import permission from '@/store/modules/permission'
 
 Vue.use(Vuex)
 
@@ -13,7 +13,7 @@ const store = new Vuex.Store({
     app,
     settings,
     user,
-    unit
+    permission
   },
   getters
 })

+ 79 - 0
src/store/modules/permission.js

@@ -0,0 +1,79 @@
+import { asyncRouterMap, constantRouterMap } from '@/router/config'
+
+const state = {
+  routers: constantRouterMap,
+  addRouters: []
+}
+
+const mutations = {
+  SET_ROUTERS: (state, routers) => {
+    state.addRouters = routers
+    state.routers = constantRouterMap.concat(routers)
+  }
+}
+
+/**
+ * 过滤账户是否拥有某一个权限,并将菜单从加载列表移除
+ *
+ * @param stringPermissions
+ * @param route
+ * @returns {boolean}
+ */
+// function hasPermission(stringPermissions, route) {
+//     if (route.meta && route.meta.permissions) {
+//         let flag = false
+//         for (let i = 0, len = stringPermissions.length; i < len; i++) {
+//             flag = route.meta.permissions.includes(stringPermissions[i])
+//             if (flag) {
+//                 return true
+//             }
+//         }
+//         return false
+//     }
+//     return true
+// }
+
+/**
+ * 单账户多角色时,使用该方法可过滤角色不存在的菜单
+ *
+ * @param isAdmin
+ * @param route
+ * @returns {*}
+ */
+// eslint-disable-next-line
+function checkPermission(isAdmin, route) {
+  if (route.meta && route.meta.admin) {
+    return isAdmin
+  } else {
+    return true
+  }
+}
+
+function filterAsyncRouter(routerMap, isAdmin) {
+  return routerMap.filter(route => {
+    if (checkPermission(isAdmin, route)) {
+      if (route.children && route.children.length) {
+        route.children = filterAsyncRouter(route.children, isAdmin)
+      }
+      return true
+    }
+    return false
+  })
+}
+
+const actions = {
+  generateRoutes({ commit }, isAdmin) {
+    return new Promise(resolve => {
+      const accessedRouters = filterAsyncRouter(asyncRouterMap, isAdmin)
+      commit('SET_ROUTERS', accessedRouters)
+      resolve()
+    })
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}

+ 0 - 36
src/store/modules/unit.js

@@ -1,36 +0,0 @@
-import { getList } from '@/api/unit'
-
-const getDefaultState = () => {
-  return {
-    units: []
-  }
-}
-
-const state = getDefaultState()
-
-const mutations = {
-  SET_UNITS: (state, units) => {
-    state.units = units
-  }
-}
-
-const actions = {
-  // get units
-  getUtils({ commit, state }) {
-    return new Promise((resolve, reject) => {
-      getList().then(response => {
-        commit('SET_UNITS', response.data.list)
-        resolve(response.data)
-      }).catch(error => {
-        reject(error)
-      })
-    })
-  }
-}
-
-export default {
-  namespaced: true,
-  state,
-  mutations,
-  actions
-}

+ 35 - 10
src/store/modules/user.js

@@ -1,12 +1,16 @@
 import { login, logout, getInfo } from '@/api/user'
 import { getToken, setToken, removeToken } from '@/utils/auth'
 import { resetRouter } from '@/router'
+import { Message } from 'element-ui'
 
 const getDefaultState = () => {
   return {
     token: getToken(),
-    name: '管理员',
-    avatar: `${process.env.BASE_URL}images/profileImg.png`
+    userId: '',
+    name: '',
+    avatar: `${process.env.BASE_URL}images/profileImg.png`,
+    isAdmin: false,
+    isFrozen: false
   }
 }
 
@@ -16,6 +20,9 @@ const mutations = {
   RESET_STATE: (state) => {
     Object.assign(state, getDefaultState())
   },
+  SET_USER_ID: (state, userId) => {
+    state.userId = userId
+  },
   SET_TOKEN: (state, token) => {
     state.token = token
   },
@@ -23,7 +30,15 @@ const mutations = {
     state.name = name
   },
   SET_AVATAR: (state, avatar) => {
-    state.avatar = avatar
+    if (avatar) {
+      state.avatar = avatar
+    }
+  },
+  SET_IS_ADMIN: (state, isAdmin) => {
+    state.isAdmin = isAdmin
+  },
+  SET_IS_FROZEN: (state, isFrozen) => {
+    state.isFrozen = isFrozen
   }
 }
 
@@ -32,12 +47,13 @@ const actions = {
   login({ commit }, userInfo) {
     const { username, password } = userInfo
     return new Promise((resolve, reject) => {
-      login({ username: username.trim(), password: password }).then(response => {
+      login(userInfo).then(response => {
         const { data } = response
         commit('SET_TOKEN', data.token)
         setToken(data.token)
         resolve()
       }).catch(error => {
+        Message.error(error.data.message)
         reject(error)
       })
     })
@@ -46,19 +62,28 @@ const actions = {
   // get user info
   getInfo({ commit, state }) {
     return new Promise((resolve, reject) => {
-      getInfo(state.token).then(response => {
+      getInfo().then(response => {
         const { data } = response
 
         if (!data) {
-          return reject('Verification failed, please Login again.')
+          return reject('Token 无效,请重新登录。')
         }
 
-        const { name, avatar } = data
+        const { userId, name, nickName, profilePic, isAdmin, isFrozen } = data
+
+        if (isFrozen) {
+          return reject('用户账号冻结,请与管理员联系')
+        }
 
-        commit('SET_NAME', name)
-        commit('SET_AVATAR', avatar)
+        commit('SET_USER_ID', userId)
+        commit('SET_NAME', nickName ? nickName : name)
+        commit('SET_AVATAR', profilePic)
+        commit('SET_IS_ADMIN', isAdmin)
+        commit('SET_IS_FROZEN', isFrozen)
         resolve(data)
       }).catch(error => {
+        Message.error(error.data.message)
+        removeToken()
         reject(error)
       })
     })
@@ -69,10 +94,10 @@ const actions = {
     return new Promise((resolve, reject) => {
       logout(state.token).then(() => {
         removeToken() // must remove  token  first
-        resetRouter()
         commit('RESET_STATE')
         resolve()
       }).catch(error => {
+        Message.error(error.data.message)
         reject(error)
       })
     })

+ 1 - 1
src/utils/request.js

@@ -82,7 +82,7 @@ service.interceptors.response.use(
         duration: 5 * 1000
       })
     }
-    return Promise.reject(error)
+    return Promise.reject(error.response)
   }
 )
 

+ 196 - 156
src/views/login/index.vue

@@ -1,20 +1,28 @@
 <template>
   <div class="login-container">
-    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" 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"
+      @keyup.enter.native="handleLogin"
+    >
 
       <div class="title-container">
-        <h3 class="title">Login Form</h3>
+        <h3 class="title">营养物语基础数据管理 <br> 后台登录</h3>
       </div>
 
-      <el-form-item prop="username">
+      <el-form-item prop="name">
         <span class="svg-container">
           <svg-icon icon-class="user" />
         </span>
         <el-input
-          ref="username"
-          v-model="loginForm.username"
-          placeholder="Username"
-          name="username"
+          ref="name"
+          v-model="loginForm.name"
+          placeholder="请输入用户名"
+          name="name"
           type="text"
           tabindex="1"
           auto-complete="on"
@@ -30,7 +38,7 @@
           ref="password"
           v-model="loginForm.password"
           :type="passwordType"
-          placeholder="Password"
+          placeholder="请输入密码"
           name="password"
           tabindex="2"
           auto-complete="on"
@@ -41,11 +49,23 @@
         </span>
       </el-form-item>
 
+      <el-form-item prop="captcha">
+        <el-row>
+          <el-col :span="14">
+            <el-input v-model="loginForm.captcha" placeholder="验证码">
+            </el-input>
+          </el-col>
+          <el-col :span="10" class="login-captcha">
+            <img :src="captchaPath" @click="getCaptcha()" alt="">
+          </el-col>
+        </el-row>
+      </el-form-item>
+
       <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login</el-button>
 
       <div class="tips">
         <span style="margin-right:20px;">username: admin</span>
-        <span> password: any</span>
+        <span> password: 123456</span>
       </div>
 
     </el-form>
@@ -53,185 +73,205 @@
 </template>
 
 <script>
-import { validUsername } from '@/utils/validate'
-
-export default {
-  name: 'Login',
-  data() {
-    const validateUsername = (rule, value, callback) => {
-      if (!validUsername(value)) {
-        callback(new Error('Please enter the correct user name'))
-      } else {
-        callback()
+  import { validUsername } from '@/utils/validate'
+  import { getUUID } from '@/utils'
+
+  export default {
+    name: 'Login',
+    data() {
+      const validateUsername = (rule, value, callback) => {
+        if (!validUsername(value)) {
+          callback(new Error('用户名不能为空'))
+        } else {
+          callback()
+        }
       }
-    }
-    const validatePassword = (rule, value, callback) => {
-      if (value.length < 6) {
-        callback(new Error('The password can not be less than 6 digits'))
-      } else {
-        callback()
+      const validatePassword = (rule, value, callback) => {
+        if (value.length < 3) {
+          callback(new Error('密码长度不能小于3'))
+        } else {
+          callback()
+        }
       }
-    }
-    return {
-      loginForm: {
-        username: 'admin',
-        password: '111111'
-      },
-      loginRules: {
-        username: [{ required: true, trigger: 'blur', validator: validateUsername }],
-        password: [{ required: true, trigger: 'blur', validator: validatePassword }]
-      },
-      loading: false,
-      passwordType: 'password',
-      redirect: undefined
-    }
-  },
-  watch: {
-    $route: {
-      handler: function(route) {
-        this.redirect = route.query && route.query.redirect
-      },
-      immediate: true
-    }
-  },
-  methods: {
-    showPwd() {
-      if (this.passwordType === 'password') {
-        this.passwordType = ''
-      } else {
-        this.passwordType = 'password'
+      return {
+        loginForm: {
+          username: '',
+          password: '',
+          reqId: '',
+          captcha: ''
+        },
+        loginRules: {
+          username: [{ required: true, trigger: 'blur', validator: validateUsername }],
+          password: [{ required: true, trigger: 'blur', validator: validatePassword }],
+          captcha: [{ required: true, message: '验证码不能为空', trigger: 'blur' }]
+        },
+        loading: false,
+        passwordType: 'password',
+        redirect: undefined,
+        captchaPath: ''
+      }
+    },
+    watch: {
+      $route: {
+        handler: function(route) {
+          this.redirect = route.query && route.query.redirect
+        },
+        immediate: true
       }
-      this.$nextTick(() => {
-        this.$refs.password.focus()
-      })
     },
-    handleLogin() {
-      this.$refs.loginForm.validate(valid => {
-        if (valid) {
-          this.loading = true
-          this.$store.dispatch('user/login', this.loginForm).then(() => {
-            this.$router.push({ path: this.redirect || '/' })
-            this.loading = false
-          }).catch(() => {
-            this.loading = false
-          })
+    created() {
+      this.getCaptcha()
+    },
+    methods: {
+      showPwd() {
+        if (this.passwordType === 'password') {
+          this.passwordType = ''
         } else {
-          console.log('error submit!!')
-          return false
+          this.passwordType = 'password'
         }
-      })
+        this.$nextTick(() => {
+          this.$refs.password.focus()
+        })
+      },
+      handleLogin() {
+        this.$refs.loginForm.validate(valid => {
+          if (valid) {
+            this.loading = true
+            this.$store.dispatch('user/login', this.loginForm).then(() => {
+              this.$router.push({ path: this.redirect || '/' })
+              this.loading = false
+            }).catch(() => {
+              this.loading = false
+            })
+          } else {
+            return false
+          }
+        })
+      },
+      // 获取验证码
+      getCaptcha() {
+        this.loginForm.reqId = getUUID()
+        this.captchaPath = process.env.VUE_APP_BASE_API + `/api/captcha.jpg?reqId=${this.loginForm.reqId}`
+      }
     }
   }
-}
 </script>
 
 <style lang="scss">
-/* 修复input 背景不协调 和光标变色 */
-/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
+  /* 修复input 背景不协调 和光标变色 */
+  /* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
 
-$bg:#283443;
-$light_gray:#fff;
-$cursor: #fff;
+  $bg:#283443;
+  $light_gray:#fff;
+  $cursor: #fff;
 
-@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
-  .login-container .el-input input {
-    color: $cursor;
+  @supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
+    .login-container .el-input input {
+      color: $cursor;
+    }
   }
-}
-
-/* reset element-ui css */
-.login-container {
-  .el-input {
-    display: inline-block;
-    height: 47px;
-    width: 85%;
-
-    input {
-      background: transparent;
-      border: 0px;
-      -webkit-appearance: none;
-      border-radius: 0px;
-      padding: 12px 5px 12px 15px;
-      color: $light_gray;
+
+  /* reset element-ui css */
+  .login-container {
+    .el-input {
+      display: inline-block;
       height: 47px;
-      caret-color: $cursor;
+      width: 85%;
+
+      input {
+        background: transparent;
+        border: 0px;
+        -webkit-appearance: none;
+        border-radius: 0px;
+        padding: 12px 5px 12px 15px;
+        color: $light_gray;
+        height: 47px;
+        caret-color: $cursor;
 
-      &:-webkit-autofill {
-        box-shadow: 0 0 0px 1000px $bg inset !important;
-        -webkit-text-fill-color: $cursor !important;
+        &:-webkit-autofill {
+          box-shadow: 0 0 0px 1000px $bg inset !important;
+          -webkit-text-fill-color: $cursor !important;
+        }
       }
     }
-  }
 
-  .el-form-item {
-    border: 1px solid rgba(255, 255, 255, 0.1);
-    background: rgba(0, 0, 0, 0.1);
-    border-radius: 5px;
-    color: #454545;
+    .el-form-item {
+      border: 1px solid rgba(255, 255, 255, 0.1);
+      background: rgba(0, 0, 0, 0.1);
+      border-radius: 5px;
+      color: #454545;
+    }
   }
-}
 </style>
 
 <style lang="scss" scoped>
-$bg:#2d3a4b;
-$dark_gray:#889aa4;
-$light_gray:#eee;
-
-.login-container {
-  min-height: 100%;
-  width: 100%;
-  background-color: $bg;
-  overflow: hidden;
-
-  .login-form {
-    position: relative;
-    width: 520px;
-    max-width: 100%;
-    padding: 160px 35px 0;
-    margin: 0 auto;
+  $bg:#2d3a4b;
+  $dark_gray:#889aa4;
+  $light_gray:#eee;
+
+  .login-container {
+    min-height: 100%;
+    width: 100%;
+    background-color: $bg;
     overflow: hidden;
-  }
 
-  .tips {
-    font-size: 14px;
-    color: #fff;
-    margin-bottom: 10px;
+    .login-form {
+      position: relative;
+      width: 520px;
+      max-width: 100%;
+      padding: 160px 35px 0;
+      margin: 0 auto;
+      overflow: hidden;
+    }
 
-    span {
-      &:first-of-type {
-        margin-right: 16px;
+    .login-captcha {
+      overflow: hidden;
+      img {
+        width: 100%;
+        cursor: pointer;
       }
     }
-  }
 
-  .svg-container {
-    padding: 6px 5px 6px 15px;
-    color: $dark_gray;
-    vertical-align: middle;
-    width: 30px;
-    display: inline-block;
-  }
+    .tips {
+      font-size: 14px;
+      color: #fff;
+      margin-bottom: 10px;
 
-  .title-container {
-    position: relative;
+      span {
+        &:first-of-type {
+          margin-right: 16px;
+        }
+      }
+    }
 
-    .title {
-      font-size: 26px;
-      color: $light_gray;
-      margin: 0px auto 40px auto;
-      text-align: center;
-      font-weight: bold;
+    .svg-container {
+      padding: 6px 5px 6px 15px;
+      color: $dark_gray;
+      vertical-align: middle;
+      width: 30px;
+      display: inline-block;
     }
-  }
 
-  .show-pwd {
-    position: absolute;
-    right: 10px;
-    top: 7px;
-    font-size: 16px;
-    color: $dark_gray;
-    cursor: pointer;
-    user-select: none;
+    .title-container {
+      position: relative;
+
+      .title {
+        font-size: 26px;
+        color: $light_gray;
+        margin: 0px auto 40px auto;
+        text-align: center;
+        font-weight: bold;
+      }
+    }
+
+    .show-pwd {
+      position: absolute;
+      right: 10px;
+      top: 7px;
+      font-size: 16px;
+      color: $dark_gray;
+      cursor: pointer;
+      user-select: none;
+    }
   }
-}
 </style>

+ 270 - 0
src/views/user/index.vue

@@ -0,0 +1,270 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <el-input
+        v-model="listQuery.query"
+        placeholder="请输入检索词"
+        style="width: 60%;"
+        class="filter-item"
+        @keyup.enter.native="fetchData()"
+      />
+      <el-button class="filter-item" style="margin-left: 10px;" type="primary" icon="el-icon-search" @click="fetchData">
+        检索
+      </el-button>
+      <el-button
+        class="filter-item"
+        style="margin: 0 10px 20px 0; float: right;"
+        type="success"
+        icon="el-icon-circle-plus-outline"
+        @click="handleCreate">
+        新建
+      </el-button>
+    </div>
+
+    <el-table
+      :key="tableKey"
+      v-loading="listLoading"
+      :data="list"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%;"
+    >
+      <el-table-column type="index" label="序号" align="center" width="60" />
+      <el-table-column label="工号" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.name }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="昵称" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.nickName }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="手机号" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.phone }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" align="center" width="60">
+        <template slot-scope="{row}">
+          <span>{{ row.gender | genderFilter}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="最后登录时间" align="center" width="180">
+        <template slot-scope="{row}">
+          <span>{{ row.lastLoginTime }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" width="180">
+        <template slot-scope="{row}">
+          <span>{{ row.createTime }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="{row,$index}">
+          <el-button type="primary" size="mini" @click="handleUpdate(row)">
+            更新
+          </el-button>
+          <el-button type="danger" size="mini" @click="resetPassword(row)">
+            重置密码
+          </el-button>
+          <el-button size="mini" type="info" v-if="row.isFrozen" @click="handleFrozen(row)">
+            解除冻结
+          </el-button>
+          <el-button size="mini" type="danger" v-else @click="handleFrozen(row)">
+            冻结
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="listQuery.pageNum" :limit.sync="listQuery.pageSize" @pagination="fetchData" />
+
+    <el-dialog :title="textMapping[dialogStatus]" :visible.sync="dialogFormVisible">
+      <el-form ref="dataForm" :rules="rules" :model="params" label-position="left" label-width="70px" style="width: 400px; margin-left:50px;">
+        <el-form-item label="工号" prop="name">
+          <el-input v-model="params.name" placeholder="请输入工号" />
+        </el-form-item>
+        <el-form-item label="密码" prop="password" v-if="dialogStatus==='CREATE'">
+          <el-input v-model="params.password" type="password" :show-password="true" placeholder="请输入密码" />
+        </el-form-item>
+        <el-form-item label="昵称" prop="nickName">
+          <el-input v-model="params.nickName" placeholder="请输入昵称" />
+        </el-form-item>
+        <el-form-item label="手机号" prop="phone">
+          <el-input v-model="params.phone" placeholder="请输入手机号" />
+        </el-form-item>
+        <el-form-item label="性别" prop="gender">
+          <el-select v-model="params.gender">
+            <el-option v-for="item in genders" :key="item.value" :label="item.name" :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="头像" prop="profilePic">
+          <single-image :value="params.profilePic" :show-preview="true" @success="uploadFile" style="width: 100%" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="dialogStatus==='CREATE'?createData():updateData()">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="重置用户密码" :visible.sync="resetPasswordDialog">
+      <el-form ref="dataForm" :rules="resetPasswordRules" :model="resetPasswordParams" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+        <el-form-item label="新密码" prop="newPassword">
+          <el-input v-model="resetPasswordParams.newPassword " type="password" :show-password="true" placeholder="请输入密码" style="width: 60%" />
+        </el-form-item>
+        <el-form-item label="确认新密码" prop="newPasswordSecond">
+          <el-input v-model="resetPasswordParams.newPasswordConfirm " type="password" :show-password="true" placeholder="请输入密码" style="width: 60%" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="resetPasswordDialog = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submitResetPassword">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import Pagination from '@/components/Pagination'
+  import SingleImage from '@/components/Upload/SingleImage'
+  import { getUsers, changeUserFrozen, resetUserPassword, createUser, updateUser } from '@/api/user'
+
+  const genderMapping = { 0: '未知', 1: '男', 2: '女' }
+
+  export default {
+    name: 'UserList',
+    components: { Pagination, SingleImage },
+    filters: {
+      genderFilter(value) {
+        return genderMapping[value]
+      }
+    },
+    created() {
+      this.fetchData()
+    },
+    data() {
+      return {
+        tableKey: 0,
+        listQuery: {
+          query: '',
+          pageNum: 1,
+          pageSize: 20
+        },
+        list: [],
+        rules: {
+          name: [{ required: true, message: '工号不能为空', trigger: 'blur' }],
+          password: [{ required: true, message: '密码不能为空', trigger: 'blur' }]
+        },
+        resetPasswordRules: {
+          newPassword: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
+          newPasswordConfirm: [{ required: true, message: '密码不能为空', trigger: 'blur' }]
+        },
+        listLoading: true,
+        total: 0,
+        dialogStatus: '',
+        dialogFormVisible: false,
+        params: {},
+        profilePic: '',
+        resetPasswordParams: {},
+        resetPasswordDialog: false,
+        genders: [{ value: 0, name: '未知' }, { value: 1, name: '男' }, { value: 2, name: '女' }],
+        textMapping: { 'UPDATE': '更新', 'CREATE': '创建' }
+      }
+    },
+    methods: {
+      fetchData() {
+        this.listLoading = true
+        getUsers(this.listQuery).then(res => {
+          this.list = res.data.list
+          this.total = res.data.count
+          this.listLoading = false
+        }).catch(res => {
+          this.listLoading = false
+          this.$message.error(res.data.message)
+        })
+      },
+      handleCreate() {
+        this.dialogStatus = 'CREATE'
+        this.params = {}
+        this.$set(this.params, 'gender', 0)
+        this.dialogFormVisible = true
+      },
+      handleUpdate(row) {
+        this.dialogStatus = 'UPDATE'
+        this.params = row
+        this.dialogFormVisible = true
+      },
+      handleFrozen(row) {
+        this.listLoading = true
+        changeUserFrozen(row.id).then(res => {
+          this.fetchData()
+          this.$message.success("提交成功")
+        }).catch(res => {
+          this.fetchData()
+          this.$message.error(res.data.message)
+        })
+      },
+      createData() {
+        this.params.gender = this.gender
+        createUser(this.params).then(res => {
+          this.fetchData()
+          this.$message.success("提交成功")
+          this.dialogFormVisible = false
+        }).catch(res => {
+          this.fetchData()
+          this.$message.error(res.data.message)
+          this.dialogFormVisible = false
+        })
+      },
+      updateData() {
+        this.params.gender = this.gender
+        updateUser(this.params.id, this.params).then(res => {
+          this.fetchData()
+          this.$message.success("提交成功")
+          this.dialogFormVisible = false
+        }).catch(res => {
+          this.fetchData()
+          this.$message.error(res.data.message)
+          this.dialogFormVisible = false
+        })
+      },
+      uploadFile(value) {
+        this.$set(this.params, "profilePic", value)
+      },
+      resetPassword(row) {
+        this.resetPasswordParams = { id: row.id }
+        this.resetPasswordDialog = true
+      },
+      submitResetPassword() {
+        if (this.resetPasswordParams.newPassword !== this.resetPasswordParams.newPasswordConfirm) {
+          this.$message.error("两次输入的密码不一致")
+          return
+        }
+        resetUserPassword(this.resetPasswordParams.id, this.resetPasswordParams.newPassword).then(res => {
+          this.fetchData()
+          this.$message.success("提交成功")
+          this.resetPasswordDialog = false
+        }).catch(res => {
+          this.fetchData()
+          this.$message.error(res.data.message)
+          this.resetPasswordDialog = false
+        })
+      }
+    },
+  }
+</script>
+
+<style scoped>
+
+</style>