Flask Vue.js全栈开发|第16章:管理后台

  • 原创
  • Madman
  • /
  • /
  • 0
  • 1967 次阅读

flask vuejs 全栈开发-min.png

Synopsis: 我们需要一个后台来管理所有已注册的用户列表、他们所发布的博客文章和评论等,为了节约时间,我只是在前端菜单栏上增加了 Admin 入口,简单的演示如何管理角色、用户、博客、评论等,并没有重新开发另一个完整的后台管理系统

本系列的最新代码将持续更新到: http://www.madmalls.com/blog/post/latest-code/

1. Admin

1.1 导航栏入口

首先需要在导航栏中增加 Admin 管理后台的入口,并且结合上一章 权限管理,只有管理员才能看到这个入口

修改 front-end/src/components/Base/Navbar.vue

<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
  <li class="nav-item active">
    <router-link to="/" class="nav-link">Home <span class="sr-only">(current)</span></router-link>
  </li>
  <li class="nav-item">
    <router-link to="/ping" class="nav-link">Ping</router-link>
  </li>
  <li class="nav-item" v-if="sharedState.is_authenticated && sharedState.user_perms.includes('admin')">
    <router-link to="/admin" class="nav-link">Admin</router-link>
  </li>
</ul>

1.2 相关组件

(1)Admin.vue

创建 front-end/src/components/Admin/Admin.vue 组件:

<template>
  <div class="container g-pt-20">
    <div class="row">
      <!-- 左边菜单栏 -->
      <div class="col-lg-3 g-mb-50">
        <aside class="g-brd-around g-brd-gray-light-v4 rounded g-px-20 g-py-30">

          <h2 class="text-center g-color-primary g-font-weight-600 g-line-height-1 g-font-size-60 g-pos-rel g-mb-30">Admin</h2>

          <hr class="g-brd-gray-light-v4 g-my-30">

          <!-- 用户头像 -->
          <div v-if="user" class="text-center g-pos-rel g-mb-30">
            <div class="g-width-100 g-height-100 mx-auto mb-3">
              <img class="img-fluid rounded-circle g-brd-around g-brd-gray-light-v4 g-pa-2" v-bind:src="user._links.avatar" v-bind:alt="user.name || user.username">
            </div>

            <span class="d-block g-font-weight-500">{{ user.name || user.username }}</span>

            <router-link v-bind:to="{ path: `/user/${sharedState.user_id}` }">
              <span class="u-icon-v3 u-icon-size--xs g-color-white--hover g-bg-primary--hover rounded-circle g-pos-abs g-top-0 g-right-15 g-cursor-pointer" title="Go To Your Profile"
                    data-toggle="tooltip"
                    data-placement="top">
                <i class="icon-finance-067 u-line-icon-pro"></i>
              </span>
            </router-link>
          </div>
          <!-- End 用户头像 -->

          <hr class="g-brd-gray-light-v4 g-my-30">

          <!-- 菜单列表 -->
          <ul class="list-unstyled mb-0">
            <li class="g-pb-3">
              <router-link v-bind:to="{ name: 'AdminRoles' }" v-bind:active-class="'active g-color-primary--active g-bg-gray-light-v5--active'" v-bind:class="isAdminRoles" class="d-block align-middle u-link-v5 g-color-text g-color-primary--hover g-bg-gray-light-v5--hover rounded g-pa-3">
                <span class="u-icon-v1 g-color-gray-dark-v5 mr-2"><i class="icon-media-127 u-line-icon-pro"></i></span>
                Roles
              </router-link>
            </li>
            <li class="g-pb-3">
              <router-link v-bind:to="{ name: 'AdminUsers' }" v-bind:active-class="'active g-color-primary--active g-bg-gray-light-v5--active'" v-bind:class="isAdminUsers" class="d-block align-middle u-link-v5 g-color-text g-color-primary--hover g-bg-gray-light-v5--hover rounded g-pa-3">
                <span class="u-icon-v1 g-color-gray-dark-v5 mr-2"><i class="icon-finance-067 u-line-icon-pro"></i></span>
                Users
              </router-link>
            </li>
            <li class="g-pb-3">
              <router-link v-bind:to="{ name: 'AdminPosts' }" v-bind:active-class="'active g-color-primary--active g-bg-gray-light-v5--active'" class="d-block align-middle u-link-v5 g-color-text g-color-primary--hover g-bg-gray-light-v5--hover rounded g-pa-3">
                <span class="u-icon-v1 g-color-gray-dark-v5 mr-2"><i class="icon-education-008 u-line-icon-pro"></i></span>
                Posts
              </router-link>
            </li>
            <li class="g-py-3">
              <router-link v-bind:to="{ name: 'AdminComments' }" v-bind:active-class="'active g-color-primary--active g-bg-gray-light-v5--active'" class="d-block align-middle u-link-v5 g-color-text g-color-primary--hover g-bg-gray-light-v5--hover rounded g-pa-3">
                <span class="u-icon-v1 g-color-gray-dark-v5 mr-2"><i class="icon-finance-206 u-line-icon-pro"></i></span>
                Comments
              </router-link>
            </li>
          </ul>
          <!-- End 菜单列表 -->
        </aside>
      </div>
      <!-- End 左边菜单栏 -->

      <!-- 右边子路由匹配后,显示对应的组件 -->
      <div class="col-lg-9 g-mb-50">
        <div class="rounded g-brd-around g-brd-gray-light-v4 g-overflow-x-scroll g-overflow-x-visible--lg g-pa-30">
          <router-view></router-view>
        </div>
      </div>
      <!-- End 嵌套路由 -->
    </div>
  </div>
</template>

<script>
import store from '../../store'

export default {
  name: 'Admin',  // this is the name of the component
  data () {
    return {
      sharedState: store.state,
      user: ''
    }
  },
  computed: {
    isAdminRoles: function () {
      const tabs = ['AdminRoles', 'AdminUsers', 'AdminEditUser', 'AdminPosts', 'AdminComments']
      if (tabs.indexOf(this.$route.name) == -1) {
        return 'active g-color-primary--active g-bg-gray-light-v5--active'
      } else {
        return ''
      }
    },
    isAdminUsers: function () {
      if (this.$route.name == 'AdminEditUser') {
        return 'active g-color-primary--active g-bg-gray-light-v5--active'
      } else {
        return ''
      }
    }
  },
  methods: {
    getUser (id) {
      const path = `/api/users/${id}`
      this.$axios.get(path)
        .then((response) => {
          // handle success
          this.user = response.data
        })
        .catch((error) => {
          // handle error
          console.error(error)
        })
    }
  },
  created () {
    const user_id = this.sharedState.user_id
    this.getUser(user_id)
    // tooltip
    $(document).ready(function(){
      $('[data-toggle="tooltip"]').tooltip(); 
    })
  }
}
</script>

(2)Roles.vue

创建 front-end/src/components/Admin/Roles.vue 组件:

<template>
  <div>
    角色列表
  </div>
</template>

<script>
export default {
  name: 'Roles',  // this is the name of the component
}
</script>

(3)Users.vue

创建 front-end/src/components/Admin/Users.vue 组件:

<template>
  <div>
    用户列表
  </div>
</template>

<script>
export default {
  name: 'Users',  // this is the name of the component
}
</script>

(4)Posts.vue

创建 front-end/src/components/Admin/Posts.vue 组件:

<template>
  <div>
    博客列表
  </div>
</template>

<script>
export default {
  name: 'Posts',  // this is the name of the component
}
</script>

(5)Comments.vue

创建 front-end/src/components/Admin/Comments.vue 组件:

<template>
  <div>
    评论列表
  </div>
</template>

<script>
export default {
  name: 'Comments',  // this is the name of the component
}
</script>

1.3 前端路由

最后,增加前端路由,修改 front-end/src/router/index.js

...
// 管理后台
import Admin from '@/components/Admin/Admin.vue'
import AdminRoles from '@/components/Admin/Roles.vue'
import AdminUsers from '@/components/Admin/Users.vue'
import AdminPosts from '@/components/Admin/Posts.vue'
import AdminComments from '@/components/Admin/Comments.vue'
...

const router = new Router({
  // mode: 'history',
  scrollBehavior,  // 不用这个,在需要跳转的改用 vue-scrollto
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    ...
    {
      // 管理后台
      path: '/admin',
      component: Admin,
      children: [
        { path: '', component: AdminRoles },
        { path: 'roles', name: 'AdminRoles', component: AdminRoles },
        { path: 'users', name: 'AdminUsers', component: AdminUsers },
        { path: 'posts', name: 'AdminPosts', component: AdminPosts },
        { path: 'comments', name: 'AdminComments', component: AdminComments }
      ],
      meta: {
        requiresAuth: true,
        requiresAdmin: true
      }
    },
    {
      path: '/ping',
      name: 'Ping',
      component: Ping
    }
  ]
})

router.beforeEach((to, from, next) => {
  const token = window.localStorage.getItem('madblog-token')
  if (token) {
    var payload = JSON.parse(atob(token.split('.')[1]))

    var user_perms = payload.permissions.split(",")
  }

  if (to.matched.some(record => record.meta.requiresAuth) && (!token || token === null)) {
    // 1. 用户未登录,但想访问需要认证的相关路由时,跳转到 登录 页
    Vue.toasted.show('Please log in to access this page.', { icon: 'fingerprint' })
    next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  } else if (token && !payload.confirmed && to.name != 'Unconfirmed') {
    // 2. 用户刚注册,但是还没确认邮箱地址时,全部跳转到 认证提示 页面
    Vue.toasted.show('Please confirm your accout to access this page.', { icon: 'fingerprint' })
    next({
      path: '/unconfirmed',
      query: { redirect: to.fullPath }
    })
  } else if (token && payload.confirmed && to.name == 'Unconfirmed') {
    // 3. 用户账户已确认,但又去访问 认证提示 页面时不让他过去
    next({
      path: '/'
    })
  } else if (token && (to.name == 'Login' || to.name == 'Register' || to.name == 'ResetPasswordRequest' || to.name == 'ResetPassword')) {
    // 4. 用户已登录,但又去访问 登录/注册/请求重置密码/重置密码 页面时不让他过去
    next({
      path: from.fullPath
    })
  } else if (to.matched.some(record => record.meta.requiresAdmin) && token && !user_perms.includes('admin')) {
    // 5. 普通用户想在浏览器地址中直接访问 /admin ,提示他没有权限,并跳转到首页
    Vue.toasted.error('403: Forbidden', { icon: 'fingerprint' })
    next({
      path: '/'
    })
  } else if (to.matched.length === 0) {
    // 6. 要前往的路由不存在时
    Vue.toasted.error('404: Not Found', { icon: 'fingerprint' })
    if (from.name) {
      next({
        name: from.name
      })
    } else {
      next({
        path: '/'
      })
    }
  } else {
    // 7. 正常路由出口
    next()
  }
})

export default router

效果图如下:

1 admin入口

注意: 如果用户没有 admin 权限,他直接访问 http://localhost:8080/#/admin 会报 403: Forbidden 错误,并被重定向到首页!

2. 角色管理

2.1 角色对象转换成 JSON

修改 back-end/app/models.py

class Role(PaginatedAPIMixin, db.Model):
    ...
    def to_dict(self):
        data = {
            'id': self.id,
            'slug': self.slug,
            'name': self.name,
            'default': self.default,
            'permissions': self.permissions,
            '_links': {
                'self': url_for('api.get_role', id=self.id)
            }
        }
        return data

    def from_dict(self, data):
        for field in ['slug', 'name', 'permissions']:
            if field in data:
                setattr(self, field, data[field])

2.2 角色列表

创建 back-end/app/api/roles.py 文件:

from flask import request, url_for, jsonify
from app.api import bp
from app.api.auth import token_auth
from app.api.errors import bad_request
from app.extensions import db
from app.models import Role
from app.utils.decorator import admin_required


@bp.route('/roles', methods=['GET'])
@token_auth.login_required
@admin_required
def get_roles():
    '''返回所有角色的集合'''
    page = request.args.get('page', 1, type=int)
    per_page = min(request.args.get('per_page', 10, type=int), 100)
    data = Role.to_collection_dict(Role.query, page, per_page, 'api.get_roles')
    return jsonify(data)

修改 front-end/src/components/Admin/Roles.vue

<template>
  <div>
    <!-- Striped Rows -->
    <div class="card g-brd-teal rounded-0 g-mb-30">
      <h3 class="card-header g-bg-teal g-brd-transparent g-color-white g-font-size-16 rounded-0 mb-0">
        <i class="fa fa-edit g-mr-5"></i>
        角色列表
      </h3>

      <div class="table-responsive">
        <table class="table table-striped u-table--v1 mb-0">
          <thead>
            <tr>
              <th>#</th>
              <th>Slug</th>
              <th class="hidden-sm">Name</th>
              <th>Permissions</th>
              <th>Action</th>
            </tr>
          </thead>

          <tbody>

            <tr v-for="(role, index) in roles.items" v-bind:key="index">
              <th scope="row">{{ index+1 }}</th>
              <td>{{ role.slug }}</td>
              <td class="hidden-sm">{{ role.name }}</td>
              <td>{{ role.permissions }}</td>
              <td>
                操作
              </td>
            </tr>

          </tbody>
        </table>
      </div>
    </div>
    <!-- End Striped Rows -->

    <!-- Pagination #04 -->
    <div v-if="roles">
      <pagination
        v-bind:cur-page="roles._meta.page"
        v-bind:per-page="roles._meta.per_page"
        v-bind:total-pages="roles._meta.total_pages">
      </pagination>
    </div>
    <!-- End Pagination #04 -->
  </div>
</template>

<script>
import Pagination from '../Base/Pagination'

export default {
  name: 'Roles',  // this is the name of the component
  components: {
    Pagination
  },
  data () {
    return {
      roles: ''
    }
  },
  methods: {
    getRoles () {
      let page = 1
      let per_page = 10
      if (typeof this.$route.query.page != 'undefined') {
        page = this.$route.query.page
      }

      if (typeof this.$route.query.per_page != 'undefined') {
        per_page = this.$route.query.per_page
      }

      const path = `/api/roles/?page=${page}&per_page=${per_page}`
      this.$axios.get(path)
        .then((response) => {
          // handle success
          this.roles = response.data
        })
        .catch((error) => {
          // handle error
          console.error(error)
        })
    }
  },
  created () {
    this.getRoles()
  },
  // 当路由变化后(比如变更查询参数 page 和 per_page)重新加载数据
  beforeRouteUpdate (to, from, next) {
    next()
    this.getRoles()
  }
}
</script>

2.3 添加角色

(1)后端 API

修改 back-end/app/api/roles.py 文件:

@bp.route('/roles/perms', methods=['GET'])
def get_perms():
    '''获取所有Permissions'''
    data = [
        {'name': 'FOLLOW', 'dec': 1},
        {'name': 'COMMENT', 'dec': 2},
        {'name': 'WRITE', 'dec': 4},
        {'name': 'ADMIN', 'dec': 128}
    ]
    return jsonify(data)


@bp.route('/roles', methods=['POST'])
@token_auth.login_required
@admin_required
def create_role():
    '''注册一个新角色'''
    data = request.get_json()
    if not data:
        return bad_request('You must post JSON data.')

    message = {}
    if 'slug' not in data or not data.get('slug', None).strip():
        message['slug'] = 'Please provide a valid slug.'
    if 'name' not in data or not data.get('name', None).strip():
        message['name'] = 'Please provide a valid name.'

    if Role.query.filter_by(slug=data.get('slug', None)).first():
        message['slug'] = 'Please use a different slug.'
    if message:
        return bad_request(message)

    permissions = 0
    for perm in data.get('permissions', 0):
        permissions += perm
    data['permissions'] = permissions

    role = Role()
    role.from_dict(data)
    db.session.add(role)
    db.session.commit()

    response = jsonify(role.to_dict())
    response.status_code = 201
    # HTTP协议要求201响应包含一个值为新资源URL的Location头部
    response.headers['Location'] = url_for('api.get_role', id=role.id)
    return response

(2)添加角色的表单

我们要添加新的角色,需要包含创建新角色的表单,所以创建 front-end/src/components/Admin/AddRole.vue

<template>
  <div>
    <h1>Add Role</h1>
    <form @submit.prevent="onSubmit">
      <div class="form-group" v-bind:class="{'u-has-error-v1': roleForm.slugError}">
        <label for="slug">Slug</label>
        <input type="text" v-model="roleForm.slug" class="form-control" id="slug" placeholder="">
        <small class="form-control-feedback" v-show="roleForm.slugError">{{ roleForm.slugError }}</small>
      </div>
      <div class="form-group" v-bind:class="{'u-has-error-v1': roleForm.nameError}">
        <label for="name">Name</label>
        <input type="text" v-model="roleForm.name" class="form-control" id="name" placeholder="">
        <small class="form-control-feedback" v-show="roleForm.nameError">{{ roleForm.nameError }}</small>
      </div>
      <div class="form-group">
        <label for="permissions">Permissions</label>
        <div>
          <!-- Inline Checkboxes -->
          <div v-for="(perm, index) in perms" v-bind:key="index" class="form-check form-check-inline mb-0">
            <label class="form-check-label mr-2">
              <input class="form-check-input mr-1" type="checkbox" v-bind:id="perm.dec" v-bind:value="perm.dec" v-model="checkPerms">{{ perm.name }}
            </label>
          </div>
          <!-- End Inline Checkboxes -->
        </div>
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </div>
</template>

<script>
import store from '../../store'

export default {
  name: 'AddRole',  //this is the name of the component
  data () {
    return {
      sharedState: store.state,
      roleForm: {
        slug: '',
        name: '',
        permissions: [],
        errors: 0,  // 表单是否在前端验证通过,0 表示没有错误,验证通过
        slugError: null,
        nameError: null
      },
      perms: null,
      checkPerms: []
    }
  },
  methods: {
    getPerms () {
      const path = `/api/roles/perms`
      this.$axios.get(path)
        .then((response) => {
          // handle success
          this.perms = response.data
        })
        .catch((error) => {
                                
                            
未经允许不得转载: LIFE & SHARE - 王颜公子 » Flask Vue.js全栈开发|第16章:管理后台

分享

作者

作者头像

Madman

如需 Linux / Python 相关问题付费解答,请按如下方式联系我

0 条评论

暂时还没有评论.

专题系列