Vue Web开发(十一)

发布于:2024-12-23 ⋅ 阅读:(15) ⋅ 点赞:(0)

1. 登录功能

  本节课完成登录页面、登录接口以及退出登录。
在这里插入图片描述
在这里插入图片描述

1.1. 新建路由/login。

  在router文件中添加路径为/login的路由,引入视图,注意是与路径“/”为兄弟关系。

//src/router/index.js
    {
        path:'/login',
        name:'login',
        component:()=>import('../view/login/login.vue')
    }

  在view下新建login文件夹,新建login.vue,模板页面。

//src/views/login/login.vue
<template>
    <div>登录页面</div>
</template>
<script>
    export default{
        name:'login',
        data(){
            return{}
        }
    }
</script>

1.2. login页面

  (1)页面有账号和密码两个表单域,引入表单el-form,添加属性,添加model属性“form”,states-icon表示表单校验图标,rules表单校验,新建校验名为“rules”,新建ref属性为“form”,以此能拿return里的form内容。label-width为名称的宽度。在return中定义rules{}。rules为一个对象,里面有两个数组对象,一个是username,一个是password。每个对象包含三个属性。第一个对象是required,必填,属性为true,校验不通过信息message,触发方式trigger为blur(布尔类型)。对二哥对象为长度限制,min为最小长度。

            return{
                form:{},
                rules:{
                    username:[
                        {
                            required:true,message:"请输入用户名",trigger:"blur"
                        },
                        {
                            min:3,
                            message:"用户名长度不得小于3位",
                            trigger:"blur"
                        },
                    ],
                    password:[
                        {
                            required:true,message:"请输入密码",trigger:"blur"
                        }
                    ]
                }
            }

  (2)开始填写卡片里的内容,新建h3标签,表单域用el-form-item标签,label为表单域名称,label-width为名称的宽度,prop属性绑定rules(?有疑问,为什么prop估计可以接受return里到的数据),el-input绑定v-model很重要,绑定form下的form.username,password同理。现在form为空,所以不好理解。加上登录按钮。

<template>
    <el-form
        :model="form"
        status-icon
        :rules="rules"
        ref="form"
        label-width="100px"
        class="login-container">
        <h3 class="login-title">系统登录</h3>
        <el-form-item
            label="用户名"
            label-width="80px"
            prop="username"
            class="username">
                <el-input
                type="input"
                v-model="form.username"
                autocomplete="off"
                placeholder="请输入账号">
                </el-input>    
        </el-form-item>
        <el-form-item
            label="密码"
            label-width="80px"
            prop="password"
            >
                <el-input
                type="password"
                v-model="form.password"
                autocomplete="off"
                placeholder="请输入密码">
                </el-input>    
        </el-form-item>
        <el-form-item class="login_submit">
            <el-button type="primary" @click="login" class="login_submit">登录</el-button>
        </el-form-item>
    </el-form>
</template>

  (3)更改样式

<style lang="less" scoped>
.login-container{
    border-width: 15px;
    background-clip: padding-box;
    margin: 180px auto;
    width:350px;
    padding: 35px 35px 15px 35px;
    background-color: #fff;
    border: 1px solid #eaeaea;
    box-shadow: 0 0 25px #cac6c6;
}
.login-title{
    margin: 0px auto 40px auto;
    text-align: center;
    color: #505458;
}
.login_submit{
    margin: 10px auto 0px auto;
}
</style>

  (4)编写login()方法,token直接用Mock.random()方法生成随机数,调用setToken并传入token。这里登录先写死。

        methods:{
            login(){
                const token = Mock.random.guid()
                this.$store.commit('setToken',token)
                this.$router.push({ name:'home' })
            }
        }

  (5)src/views/login/logon.vue完整代码

//src/views/login/logon.vue
<template>
    <el-form
            :model="form"
            status-icon
            :rules="rules"
            ref="form"
            label-width="100px"
            class="login-container">
        <h3 class="login-title">系统登录</h3>
        <el-form-item
                label="用户名"
                label-width="80px"
                prop="username"
                class="username">
            <el-input
                    type="input"
                    v-model="form.username"
                    autocomplete="off"
                    placeholder="请输入账号">
            </el-input>
        </el-form-item>
        <el-form-item
                label="密码"
                label-width="80px"
                prop="password">
            <el-input
                    type="password"
                    v-model="form.password"
                    autocomplete="off"
                    placeholder="请输入密码">
            </el-input>
        </el-form-item>
        <el-form-item class="login_submit">
            <el-button type="primary" @click="login" class="login_submit">登录</el-button>
        </el-form-item>
    </el-form>
</template>
<script>
    export default {
        name: 'login',
        data() {
            return {
                form: {},
                rules: {
                    username: [
                        {
                            required: true,
                            message: "请输入用户名",
                            trigger: "blur"
                        },
                        {
                            min: 3,
                            message: "用户名长度不得小于3位",
                            trigger: "blur"
                        },
                    ],
                    password: [
                        {
                            required: true,
                            message: "请输入密码",
                            trigger: "blur"
                        }
                    ]
                }
            }
        }
    }
</script>
<style lang="less" scoped>
    .login-container {
        border-width: 15px;
        background-clip: padding-box;
        margin: 180px auto;
        width: 350px;
        padding: 35px 35px 15px 35px;
        background-color: #fff;
        border: 1px solid #eaeaea;
        box-shadow: 0 0 25px #cac6c6;
    }

    .login-title {
        margin: 0px auto 40px auto;
        text-align: center;
        color: #505458;
    }

    .login_submit {
        margin: 10px auto 0px auto;
    }
</style>


1.3. 登录权限凭证token

  登录需要用到token,用户输入账户密码,前端调用接口将数据传送给后端,后端根据数据库校验,校验成功后给出一个登录凭证,也就是“token”。
  (1)安装依赖第三方库js-cookie,cnpm i js-cookie@3.0.5,
在这里插入图片描述
  (2)在store中新建user.js,引入cookie,向外暴露,定义一个state,定义token为空,mutations编几个方法用来改变token,setToken()接收两个参数state、val,val为val赋值给token。调用cookie的set方法,接收两个参数,第一个是cookie的名称,第二个是传入的值val,清除cookie方法clearToken()方法,方法同理,将token置空,cookie的remove方法将token清空(?),获取token方法getToken()方法,获取token调用cookie的get方法,如果当前缓存中有token,我们直接cookie.get(token名称)获取,如果没有,要从state中获取,双竖线表示容错。

import Cookie from 'js-cookie'
export default{
    state:{
        token:'',
    },
    mutations:{
        setToken(state,val){
            state.token=val
            Cookie.set('token',val)
        },
        clearToken(){
            state.token = ''
            Cookie.remove('token')
        },
        getToken(state){
            // state.token = state.token || Cookie.get('token')
            state.token = Cookie.get('token') || state.token
        }
    }
}

  (3)注意需要在store中引入user.js,并且向外暴露。

import tab from './tab'
import user from './user'
Vue.use(Vuex)
export default new Vuex.Store({
    modules:{
        tab,
        user
    }
})

  引入user.js
在这里插入图片描述

1.4. 路由导航守卫

  更改main.js,路由进行跳转的时候,我们可以通过导航守卫监听,router.beforeEach()方法可以监听,接收一个箭头函数,接收三个参to,from,next。commit调用getToken(commit用法?),这样做是为了防止页面刷新后丢失了token信息,这里需要一个判断,如果token不存在并且当前页不是/login,那么我们直接返回/login页,to.name表示当前页。如果是其他情况,那么正常跳转即可。
在这里插入图片描述

//src/mian.js
router.beforeEach((to,from,next)=>{
  store.commit('getToken')
  const token = store.state.user.token
  if(!token && to.name !=='login'){
    next({ name:'login' })
  }else{
      next()
  }
})

在这里插入图片描述

1.5. login登录事件

  我们给系统添加一个登最凭还叫"token”,这个tokn在登录的时候通过接口请求将用户名和密码传给后端,后端再数报库中匹登成功后返回一个凭证,前端将token缓存起来,再调用接口时传给后端验证就建立了登录权限校验。
  (1)在src/api/mockServeData文件夹下新建permission.js。

// src/api/mockServeData/permission.js
import Mock from 'mockjs'
export default {
  getMenu: config => {
    const { username, password } = JSON.parse(config.body)
    // 先判断用户是否存在
    // 判断账号和密码是否对应
    //menuList用于后面做权限分配,也就是用户可以展示的菜单
    if (username === 'admin' && password === 'admin') {
      return {
        code: 200,
        data: {
          menuList: [
            {
              path: '/home',
              name: 'home',
              label: '首页',
              icon: 'house',
              url: 'Home'
            },
            {
              path: '/mall',
              name: 'mall',
              label: '商品管理',
              icon: 'video-play',
              url: 'Mall'
            },
            {
              path: '/user',
              name: 'user',
              label: '用户管理',
              icon: 'user',
              url: 'User'
            },
            {
              path: 'other',
              label: '其他',
              icon: 'location',
              children: [
                {
                  path: '/page1',
                  name: 'page1',
                  label: '页面1',
                  icon: 'setting',
                  url: 'Page1'
                },
                {
                  path: '/page2',
                  name: 'page2',
                  label: '页面2',
                  icon: 'setting',
                  url: 'Page2'
                }
              ]
            }
          ],
          token: Mock.Random.guid(),
          message: '获取成功'
        }
      }
    } else if (username === 'xiaoxiao' && password === 'xiaoxiao') {
      return {
        code: 200,
        data: {
          menuList: [
            {
              path: '/home',
              name: 'home',
              label: '首页',
              icon: 'house',
              url: 'Home'
            },
            {
              path: '/user',
              name: 'user',
              label: '用户管理',
              icon: 'user',
              url: 'User'
            }
          ],
          token: Mock.Random.guid(),
          message: '获取成功'
        }
      }
    } else {
      return {
        code: -999,
        data: {
          message: '密码错误'
        }
      }
    }
  }
}

  (2)引用permission.js。
在这里插入图片描述

//mock.js
import Mock from 'mockjs'
import homeApi from './mockServeData/home.js'
import userApi from './mockServeData/user.js'
import permissionApi from './mockServeData/permission'
Mock.mock('/home/getData',homeApi.getStatisticalData)
Mock.mock(/user\/add/,'post',userApi.createUser)
Mock.mock(/user\/edit/,'post',userApi.updateUser)
Mock.mock(/user\/getUser/,'get',userApi.getUserList)
Mock.mock(/user\/del/,'post',userApi.deleteUser)
Mock.mock(/permission\/getMenu/,'post',permissionApi.getMenu)

  (3)接口对外暴露。
在这里插入图片描述

// src/api/data.js
// 这里引入的是文件,不是依赖
import axios from './axios'

/**
 * 接口对外暴露
 * @param param
 * @returns {*}
 */
export const getMenu = (param) => {
    return axios.request({
        url:'/permission/getMenu',
        method:'post',
        data:param
    })
}
/**
 * 接口对外暴露
 */
export const getData = () =>{
    return axios.request({
        url:'/home/getData'
    })
}
/**
 * 接口对外暴露
 */
export const getUser = (params) => {
    return axios.request({
        url:'/user/getUser',
        method:'get',
        params
    })
}

  (4)在src/views/login/logon.vue调用

    import {getMenu} from '@/api/data'

  编写login()方法,调用getMenu并传入token。

  methods:{
            login(){
                getMenu(this.form).then(res=>{
                    if (res.code===2000){
                        //成功
                    }else {
                        //异常通过element ui方法
                        this.$message.warning(res.data.message)
                    }
                })
                // const token = Mock.random.guid()
                // this.$store.commit('setToken',token)
                // this.$router.push({ name:'home' })
            }
        }

  (5)数据通讯
在这里插入图片描述

import Cookie from 'js-cookie'
export default {
    state: {
        isCollapse: false,
        //默认菜单面包屑
        tabsList: [{
            id: 0,
            path: "/home",
            name: "home",
            label: "首页",
            icon: "s-home",
            url: "Home/Home",
        }],
        //当前菜单面包屑
        currentMenu: null,
        menu: []
    },
    //更改 Vuex 的store中的状态的唯一方法是提交mutations
    mutations: {
        collapseMenu(state) {
            state.isCollapse = !state.isCollapse
        },
        selectMenu(state, val) {
            if (val.name !== 'home') {
                state.currentMenu = val
                const result = state.tabsList
                    .findIndex(item => item.name === val.name)
                if (result === -1) {//不存在
                    state.tabsList.push(val)
                }
            } else {
                state.currentMenu = null
            }
        },
        closeTag(state, val) {
            const result = state.tabsList.findIndex(item => item.name === val.name)
            state.tabsList.splice(result, 1)
        },
        setMenu(state, val) {
            state.menu = val
        },
        clearMenu(state) {
            state.menu = []
            Cookie.remove('menu')
        },
        addMenu(state, router) {
            if (!Cookie.get('menu')) {
                return
            }
            const menu = JSON.parse(Cookie.get('menu'))
            state.menu = menu
            const menuArray = []
            menuArray.forEach(item => {
                if (item.children) {
                    item.children = item.children.map(item2 => {
                        item2.component=()=>import(`../views/${item2.url}`)
                        return item2
                    })
                    menuArray.push(...item.children)
                }else {
                    item.component=()=>import(`../views/${item.url}`)
                    menuArray.push(item)
                }
            })
            //路由动态添加
            menuArray.forEach(item=>{
                router.addRoutes('Main',item)
            })
        }
    }
}

  (6)整理数据
在这里插入图片描述

 methods:{
            login(){
                getMenu(this.form).then(res=>{
                    if (res.code===2000){
                        //成功
                        //清除路由
                        this.$store.commit('clearMenu')
                        //设置路由
                        this.$store.commit('setMenu',res.data.menu)
                        //设置Token
                        this.$store.commit('setToken',res.data.token)
                        //动态添加路由
                        this.$store.commit('addMenu',this.$store)
                        //页面跳转
                        this.$router.push({ name:'home' })
                    }else {
                        //异常通过element ui方法
                        this.$message.warning(res.data.message)
                    }
                })
                // const token = Mock.random.guid()
                // this.$store.commit('setToken',token)
                // this.$router.push({ name:'home' })
            }
        }

  (7)CommonAside
在这里插入图片描述

<template>
    <el-menu background-color="#545c64" text-color="#fff"
             active-text-color="#ffd04b" default-active="/home"
             class="el-menu-vertical-demo"
             @open="handleOpen" @close="handleClose"
             :collapse="isCollapse">
        <!-- <h3>通用后台管理系统</h3> -->
        <h3>{{isCollapse ? '后台' : '通用后台管理系统'}}</h3>
        <!-- 一级菜单 -->
        <el-menu-item v-for="item in noChildren"
                      @click="clickMenu(item)" default-active=""
                      :index="item.path+''" :key="item.path">
            <i :class="'el-icon-'+item.icon"></i>
            <span slot="title">{{item.label}}</span>
        </el-menu-item>
        <!-- 二级菜单 -->
        <el-submenu v-for="item in hasChildren"
                    :index="item.path+''" :key="item.path">
            <template slot="title">
                <i :class="'el-icon'+item.icon"></i>
                <span slot="title">{{item.label}}</span>
            </template>
            <!-- 这里subItem和subIndex并没有实际意义,只是用来指代item.children的多个数组,
            两者甚至可以互换,只新定义一个也可以的,:index后面有没有.path都可以运行成功 -->
            <el-menu-item-group v-for="(subItem,subIndex) in item.children"
                                :key="subItem.path+''">
                <el-menu-item @click="clickMenu(subItem)" :index="subIndex.toString()">
                    {{subItem.label}}
                </el-menu-item>
            </el-menu-item-group>
        </el-submenu>
        <!--        <el-menu-item index="3" disabled>-->
        <!--            <i class="el-icon-document"></i>-->
        <!--            <span slot="title">导航三</span>-->
        <!--        </el-menu-item>-->
    </el-menu>
</template>

<script>
    export default {
        data() {
            return {
                // isCollapse: false,
                menu: [
                    // {
                    //     id: 0,
                    //     path: "/home",
                    //     name: "home",
                    //     label: "首页",
                    //     icon: "s-home",
                    //     url: "Home/Home",
                    // },
                    // {
                    //     id: 1,
                    //     path: "/mail",
                    //     name: "mail",
                    //     label: "商品管理",
                    //     icon: "video-play",
                    //     url: "MailManage/MailManage",
                    // },
                    // {
                    //     id: 2,
                    //     path: "/user",
                    //     name: "user",
                    //     label: "用户管理",
                    //     icon: "user",
                    //     url: "UserManage/UserManage",
                    // },
                    // {
                    //     label: "其他",
                    //     icon: "location",
                    //     children: [
                    //         {
                    //             id: 3,
                    //             path: "/page1",
                    //             name: "pageOne",
                    //             label: "页面1",
                    //             icon: "setting",
                    //             url: "Other/PageOne",
                    //         },
                    //         {
                    //             id: 4,
                    //             path: "/page2",
                    //             name: "pageTwo",
                    //             label: "页面2",
                    //             icon: "setting",
                    //             url: "Other/PageTwo",
                    //         }
                    //     ],
                    // },
                ]
            };
        },
        created() {
            //初始化数据
            this.initData();
        },
        methods: {
            handleOpen(key, keyPath) {
                console.log(key, keyPath);
            },
            handleClose(key, keyPath) {
                console.log(key, keyPath);
            },
            /**
             * 一级、二级菜单点击事件
             * @param item
             */
            clickMenu(item) {
                //全局引用router跳转
                this.$router.push({
                    // name: item.name //名字路由指引
                     path:item.path//路径路由指引
                }).catch(e => {
                })
                this.$store.commit('selectMenu',item)
            },
            /**
             * 初始化数据
             */
            initData() {
                //全局引用router跳转
                this.$router.push({
                    name: "home" //名字路由指引
                    // path:item.path//路径路由指引
                }).catch(e => {
                })
            }
        },
        //计算属性
        computed: {
            isCollapse() {
                return this.$store.state.tab.isCollapse
            },
            // //没有子菜单
            // noChildren() {
            //     //filter过滤没有子菜单的列表数据
            //     return this.menu.filter(item => !item.children)
            // },
            // //有子菜单
            // hasChildren() {
            //     //filter过滤有子菜单的列表数据
            //     return this.menu.filter(item => item.children)
            // },
            //没有子菜单
            noChildren() {
                //filter过滤没有子菜单的列表数据
                return this.asyncMenu.filter(item => !item.children)
            },
            //有子菜单
            hasChildren() {
                //filter过滤有子菜单的列表数据
                return this.asyncMenu.filter(item => item.children)
            },
            //添加
            asyncMenu(){
                this.$store.state.tab.menu
            }
        }
    }
</script>
//components/CommonAside.vue的style标签
<style lang="less" scoped>
    .el-menu-vertical-demo:not(.el-menu--collapse) {
        width: 200px;
        min-height: 400px;
    }

    // 这里h3是less的语法规范,需要引入,scoped表是只在该组件中使用样式,
    //less语法中,h3标签不用加点,class才要。
    .el-menu {
        height: 100%;
        border: none;

        h3 {
            color: rgb(255, 255, 255);
            text-align: center;
            line-height: 48px;
        }
    }
</style>

  (8)注释数据
在这里插入图片描述

import Vue from 'vue';
import VueRouter from 'vue-router';
import Main from '../views/Main.vue';

Vue.use(VueRouter)
const routes = [
    {
        path: '/',
        name: 'Main',
        component: Main,//全局引用
        children:[
            // {
            //     path:'/home',
            //     name:'home',
            //     component:()=>import('../views/home/index')
            // },
            // {
            //     path:'/user',
            //     name:'user',
            //     component:()=>import('../views/user/index')
            // },
            // {
            //     path:'/mail',
            //     name:'mail',
            //     component:()=>import('../views/mail/index')
            // },
            // {
            //     path:'/page1',
            //     name:'pageOne',
            //     component:()=>import('../views/other/pageOne.vue')
            // },
            // {
            //     path:'/page2',
            //     name:'pageTwo',
            //     component:()=>import('../views/other/pageTwo.vue')
            // }
        ]
    },
    {
        path:'/login',
        name:'login',
        component:()=>import('../views/login/login.vue')
    }
]
const router = new VueRouter({
    mode: 'history',
    routes
})

export default router;

  (9)刷新白屏处理
在这里插入图片描述

new Vue({
    store,
    router,
    render: h => h(App),
    created() {
        //动态添加路由菜单动态设置
        store.commit('addMenu', this.$router)
    }
}).$mount('#app')

  (10)登录状态,展示首页
在这里插入图片描述

router.beforeEach((to, from, next) => {
    console.log("w=token==")
    store.commit('getToken')
    const token = store.state.user.token
    console.log("w=token==", token)
    if (!token && to.name !== 'login') {
        next({name: 'login'})
    } else if (token && to.name === 'login') {
        //登录状态,展示首页
        next({name: 'home'})
    } else {
        next()
    }
})

  (11)退出登录
在这里插入图片描述

<el-dropdown-menu slot="dropdown">
                    <el-dropdown-item>个人中心</el-dropdown-item>
                    <el-dropdown-item @click.native="logout()">退出</el-      dropdown-item>
</el-dropdown-menu>
/**
* 退出登录
 */
logout(){
      this.$store.commit('clearToken')
      this.$store.commit('clearMenu')
       //通过路径页面跳转
      this.$router.push('/login')
}

  登录页面、登录接口以及退出登录示例下载