vue项目实战

文章目录

一、使用vueCli创建项目?

vue create my-project
在这里插入图片描述
在这里插入图片描述
cd edu-boss-fed
npm run serve

二、加入Git版本管理

可添加到gitHub 或者gitee上均可

  • 创建本地仓库 git init
  • 将⽂件添加到暂存区 git add ⽂件
  • 提交历史记录 git commit “提交⽇志”
  • 添加远端仓库地址 git remote add origin 你的远程仓库地址
  • 推送提交 git push -u origin

三、初始化目录介绍

在这里插入图片描述

四、调整目录结构

主要内容:

  • 删除初始化的默认⽂件
  • 新增调整我们需要的⽬录结构

1.修改 App.vue

<template>
  <div id="app">
    <!-- 根路由的出口 -->
    <router-view/>
  </div>
</template>

<style lang="scss" scoped>

</style>

2.修改 router/index.ts

import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'

Vue.use(VueRouter)

// 路由配置规则
const routes: Array<RouteConfig> = [
]

const router = new VueRouter({
  routes
})

export default router

3.删除默认示例⽂件:

  • src/views/About.vue
  • src/views/Home.vue
  • src/components/HelloWorld.vue
  • src/assets/logo.png

4.创建以下内容:

  • src/services ⽬录,接⼝模块
  • src/utils ⽬录,存储⼀些⼯具模块
  • src/styles ⽬录,存储⼀些样式资源

5.修改后的目录结构

在这里插入图片描述

五、使用TS开发Vue

1.环境说明

在 Vue 项目中启用 TypeScript 支持
两种方式:
(1)全新项目:使用 Vue CLI 脚手架工具创建 Vue 项目
(2)已有项目:添加 Vue 官方配置的 TypeScript 适配插件
使用 @vue/cli 安装TypeScript 插件:
vueadd@vue/typescript

2.相关配置说明

项目根目录下:tsconfig.json

3.使用OptionsAPI定义Vue组件

4. 使用ClassAPIs定义Vue组件

5.关于装饰器语法

6.使用vuePropertyDecorator创建Vue组件

7.总结创建组件方式

六、基础处理

1.导入elementUI

Element,⼀套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌⾯端组件库。

1.安装 element
npm i element-ui
2.在 main.ts 中导⼊配置

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(ElementUI)

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

3.测试使⽤
在app.vue中直接使用

<template>
  <div id="app">
    <h1>rjy</h1>
    <!-- 根路由的出口 -->
    <router-view/>
    <el-row>
  <el-button>默认按钮</el-button>
  <el-button type="primary">主要按钮</el-button>
  <el-button type="success">成功按钮</el-button>
  <el-button type="info">信息按钮</el-button>
  <el-button type="warning">警告按钮</el-button>
  <el-button type="danger">危险按钮</el-button>
</el-row>
  </div>
</template>

<style lang="scss" scoped>
</style>

2.样式的处理

src/styles
|-- index.scss 全局样式(在入口模块加载生效)
|-- mixin.scss 公共的mixin混入(可以吧重复的样式封装为mixin混入到复用的地方)
|-- reset.scss 重置基础样式
|-- variables.scss 公共样式变量

variables.scss

$primary-color: #40586F;
$success-color: #51cf66;
$warning-color: #fcc419;
$danger-color: #ff6b6b;
$info-color: #868e96; // #22b8cf;

$body-bg: #E9EEF3; // #f5f5f9;

$sidebar-bg: #F8F9FB;
$navbar-bg: #F8F9FB;

$font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

index.scss

@import './variables.scss';

// globals
html {
  font-family: $font-family;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  // better Font Rendering
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  margin: 0;
  background-color: $body-bg;
}

// custom element theme
$--color-primary: $primary-color;
$--color-success: $success-color;
$--color-warning: $warning-color;
$--color-danger: $danger-color;
$--color-info: $info-color;
// change font path, required
$--font-path: '~element-ui/lib/theme-chalk/fonts';
// import element default theme
@import '~element-ui/packages/theme-chalk/src/index';
// node_modules/element-ui/packages/theme-chalk/src/common/var.scss

// overrides

// .el-menu-item, .el-submenu__title {
//   height: 50px;
//   line-height: 50px;
// }

.el-pagination {
  color: #868e96;
}

// components

.status {
  display: inline-block;
  cursor: pointer;
  width: .875rem;
  height: .875rem;
  vertical-align: middle;
  border-radius: 50%;

  &-primary {
    background: $--color-primary;
  }

  &-success {
    background: $--color-success;
  }

  &-warning {
    background: $--color-warning;
  }

  &-danger {
    background: $--color-danger;
  }

  &-info {
    background: $--color-info;
  }
}

app.vue中加载
记得index引入时添加后缀index.scss

// 加载全局样式
import './styles/index.scss'

3.共享全局样式变量

1、app.vue使用variables中的变量

<template>
  <div id="app">
    <h1>rjy</h1>
    <!-- 根路由的出口 -->
    <router-view/>
<p class="text">hello world</p>
  </div>
</template>

<style lang="scss" scoped>
@import "~@/styles/variables.scss";
.text{
  color: $success-color
}

</style>

2、每个文件均想引用variables.scss中的样式,
全局注册css

1 项目根目录下创建vue.config.js

module.exports = {
    css: {
      loaderOptions: {
        // 默认情况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时生效
        // 因为 `scss` 语法在内部也是由 sass-loader 处理的
        // 但是在配置 `prependData` 选项的时候
        // `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号
        // 在这种情况下,我们可以使用 `scss` 选项,对 `scss` 语法进行单独配置
        scss: {
          prependData: `@import "~@/styles/variables.scss";`
        }
        
      }
    }
  }

修改完后需要重启

4.配置接口

后台为我们提供了数据接口,分别是:

配置客户端层面的服务端代理跨域可以参考官方文档中的说明:

下面是具体的操作流程。
在项目根目录下添加vue.config.js配置文件。

module.exports = {
    css: {
      loaderOptions: {
        // 默认情况下 `sass` 选项会同时对 `sass` 和 `scss` 语法同时生效
        // 因为 `scss` 语法在内部也是由 sass-loader 处理的
        // 但是在配置 `prependData` 选项的时候
        // `scss` 语法会要求语句结尾必须有分号,`sass` 则要求必须没有分号
        // 在这种情况下,我们可以使用 `scss` 选项,对 `scss` 语法进行单独配置
        scss: {
            prependData: `@import "~@/styles/variables.scss";`
        }
        
      }
    },
    devServer: {
        proxy: {
          '/boss': {
            target: 'http://eduboss.lagou.com',
            changeOrigin: true // 把请求头中的 host 配置为 target
          },
          '/front': {
            target: 'http://edufront.lagou.com',
            changeOrigin: true
          }
        }
      }
  }

5.封装请求模块

安装 axios:npm i axios
创建 src/utils/request.ts :

import axios from 'axios'

const request = axios.create({
    //配置选项
    //baseURL
    //timeout

})

//请求拦截器
//响应拦截器
export default request

测试:
app.vue中添加测试代码:

<template>
  <div id="app">
    <h1>rjy</h1>
    <!-- 根路由的出口 -->
    <router-view/>
<p class="text">hello world</p>
  </div>
</template>

<script lang='ts'>
import Vue from 'vue'
import request from '@/utils/request'

request({
  method: 'GET',
  url: '/boss'
}).then(res => {
  console.log(res)
})
export default Vue.extend({
  name: 'App'
})

</script>

<style lang="scss" scoped>
.text{
  color: $primary-color
}

</style>

结果:
在这里插入图片描述

七 布局

1.初始化路由页面

我们这⾥先把这⼏个主要的⻚⾯配置出来,其它⻚⾯在随后的开发过程中配置。

路径说明
/首页
/login⽤户登录
/role⻆⾊管理
/menu菜单管理
/resource资源管理
/course课程管理
/user⽤户管理
/advert⼴告管理
/advert-space⼴告位管理

views文件夹下添加对应的文件

router/index.ts 文件中添加

import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'

Vue.use(VueRouter)

// 路由配置规则
const routes: Array<RouteConfig> = [
  {
    path: '/',
    name: 'home',
    component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
  },
  {
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
  },
  {
    path: '/role',
    name: 'role',
    component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue')
  },
  {
    path: '/menu',
    name: 'menu',
    component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue')
  },
  {
    path: '/resource',
    name: 'resource',
    component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue')
  },
  {
    path: '/course',
    name: 'course',
    component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue')
  },
  {
    path: '/user',
    name: 'user',
    component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue')
  },
  {
    path: '/advert',
    name: 'advert',
    component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue')
  },
  {
    path: '/advert-space',
    name: 'advert-space',
    component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
  },
  {
    path: '*',
    name: '404',
    component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
  }

]

const router = new VueRouter({
  routes
})

export default router

2.Layout和嵌套路由

src目录下新建layout文件,index.vue文件

<template>
  <div class="lauout">布局组件
  <!-- 子路由页面 -->
  <router-view/>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  name: 'LayoutIndex'
})
</script>

<style lang='scss'>
</style>

更改路由
router/index.ts

import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Layout from '@/layout/index.vue'

Vue.use(VueRouter)

// 路由配置规则
const routes: Array<RouteConfig> = [
  {
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
  },
  {
    path: '/',
    component: Layout,
    children: [
      {
        path: '', //默认子路由
        name: 'home',
        component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue')
      },
      {
        path: '/role',
        name: 'role',
        component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue')
      },
      {
        path: '/menu',
        name: 'menu',
        component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue')
      },
      {
        path: '/resource',
        name: 'resource',
        component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue')
      },
      {
        path: '/course',
        name: 'course',
        component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue')
      },
      {
        path: '/user',
        name: 'user',
        component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue')
      },
      {
        path: '/advert',
        name: 'advert',
        component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue')
      },
      {
        path: '/advert-space',
        name: 'advert-space',
        component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
      }
    ]
  },
  {
    path: '*',
    name: '404',
    component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
  }

]

const router = new VueRouter({
  routes
})

export default router

3.Container布局容器

处理layout,借助element组件库
可参照Container 布局容器 布局
layout/index.vue

<template>
  <el-container>
    <el-aside width="200px">Aside</el-aside>
    <el-container>
      <el-header>Header</el-header>
      <el-main>Main</el-main>
    </el-container>
  </el-container>
  <!-- 子路由页面 -->
  <!-- <router-view/> -->

</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  name: 'LayoutIndex'
})

</script>

<style lang='scss'>
.el-container {
    min-height: 100vh;
    min-width: 980px;
}
.el-aside {
  background: #d3dce6;
}

.el-header {
  background: #fff;
}

.el-main {
  background: #e9eef3;
}
</style>

4.侧边栏菜单

参照element中的menu选项
因为侧边栏代码比较臃肿,且侧边栏单独管理
所以在layout文件夹下创建components文件夹下添app-aside.vue文件

<template>
<div class="aside">
  <el-menu
      default-active="2"
      class="el-menu-vertical-demo"
      @open="handleOpen"
      @close="handleClose"
      background-color="#545c64"
      text-color="#fff"
      active-text-color="#ffd04b"
      router>
      <el-submenu index="1">
        <template slot="title">
          <i class="el-icon-location"></i>
          <span>权限管理</span>
        </template>
        <el-menu-item index="/role">
            <i class="el-icon-menu"></i>
            <span slot="title">角色管理</span>
        </el-menu-item>
        <el-menu-item index="/menu">
            <i class="el-icon-menu"></i>
            <span slot="title">菜单管理</span>
        </el-menu-item>
        <el-menu-item index="/resource">
            <i class="el-icon-menu"></i>
            <span slot="title">资源管理</span>
        </el-menu-item>
      </el-submenu>
      <el-menu-item index="/course">
        <i class="el-icon-menu"></i>
        <span slot="title">课程管理</span>
      </el-menu-item>
      <el-menu-item index="/user">
        <i class="el-icon-document"></i>
        <span slot="title">用户管理</span>
      </el-menu-item>
      <el-submenu index="4">
        <template slot="title">
          <i class="el-icon-location"></i>
          <span>广告管理</span>
        </template>
        <el-menu-item index="/advert">
            <i class="el-icon-menu"></i>
            <span slot="title">广告列表</span>
        </el-menu-item>
        <el-menu-item index="/advert-space">
            <i class="el-icon-menu"></i>
            <span slot="title">广告位列表</span>
        </el-menu-item>
      </el-submenu>
  </el-menu>
</div>
</template>

<script lang='ts'>
import Vue from 'vue'

export default Vue.extend({
  name: 'AppAside',
  methods: {
    handleOpen (key: string, keyPath: string): void{
      console.log(key, keyPath)
    },
    handleClose (key: string, keyPath: string): void {
      console.log(key, keyPath)
    }
  }
})
</script>

<style lang='scss' scoped>
.aside {
  .el-menu {
      min-height: 100vh;
  }
}
</style>

App.vue中

<template>
  <el-container>
    <el-aside width="200px">
      <app-aside />
    </el-aside>
    <el-container>
      <el-header>Header</el-header>
      <el-main>
          <router-view/>
      </el-main>
    </el-container>
  </el-container>
  <!-- 子路由页面 -->
  <!-- <router-view/> -->

</template>

<script lang="ts">
import Vue from 'vue'
import AppAside from './components/app-aside.vue'
export default Vue.extend({
  name: 'LayoutIndex',
  components: {
    AppAside
  }
})

</script>

<style lang='scss'>
.el-container {
    min-height: 100vh;
    min-width: 980px;
}
.el-aside {
  background: #d3dce6;
}

.el-header {
  background: #fff;
}

.el-main {
  background: #e9eef3;
}
</style>

5.头部Header

面包屑和登录状态

components文件夹下添app-header.vue文件

<template>
  <div class="header">
    <el-breadcrumb separator-class="el-icon-arrow-right">
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item>活动管理</el-breadcrumb-item>
      <el-breadcrumb-item>活动列表</el-breadcrumb-item>
      <el-breadcrumb-item>活动详情</el-breadcrumb-item>
    </el-breadcrumb>
    <el-dropdown>
      <span class="el-dropdown-link">
          <el-avatar shape="square" :size="30" src="https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png"></el-avatar>
          <i class="el-icon-arrow-down el-icon--right"></i></span>
      <el-dropdown-menu slot="dropdown">
      <el-dropdown-item>当前用户id</el-dropdown-item>
      <el-dropdown-item divided>退出</el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script lang='ts'>
import Vue from 'vue'

export default Vue.extend({
  name: 'AppHeader'
})
</script>

<style lang="scss" scoped>

.header{
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  .el-dropdown-link{
    display: flex;
    align-items: center;
}
}

</style>

App.vue

<template>
  <el-container>
    <el-aside width="200px">
      <app-aside />
    </el-aside>
    <el-container>
      <el-header>
        <app-header/>
      </el-header>
      <el-main>
          <router-view/>
      </el-main>
    </el-container>
  </el-container>
  <!-- 子路由页面 -->
  <!-- <router-view/> -->

</template>

<script lang="ts">
import Vue from 'vue'
import AppAside from './components/app-aside.vue'
import AppHeader from './components/app-header.vue'
export default Vue.extend({
  name: 'LayoutIndex',
  components: {
    AppAside,
    AppHeader
  }
})

</script>

<style lang='scss'>
.el-container {
    min-height: 100vh;
    min-width: 980px;
}
.el-aside {
  background: #d3dce6;
}

.el-header {
  background: #fff;
}

.el-main {
  background: #e9eef3;
}
</style>

八 登录

1.页面布局

Form表单组件
login/index.vue中

<template>
  <div class="system">
    <div class="title">Edu boss 管理系统</div>
    <div class="login">
      <el-form class = "login-form demo-ruleForm" label-position="top" ref="form" :model="form" label-width="80px" >
        <div class="denglu">登录</div>
        <el-form-item label="手机号">
            <el-input v-model="form.name"></el-input>
        </el-form-item>
        <el-form-item label="密码">
            <el-input v-model="form.name"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit" class="login-btn">登录</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  name: 'LoginIndex',
  data () {
    return {
      form: {
        name: '',
        region: '',
        date1: '',
        date2: '',
        delivery: false,
        type: [],
        resource: '',
        desc: ''
      }
    }
  },
  methods: {
    onSubmit () {
      console.log('submit!')
    }
  }
})
</script>

<style lang="scss" scoped>
.system{
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: 40px;
  .title{
    display: flex;
    margin-bottom: 50px;
    text-align: center;
    font-size: 45px;
    width: 300px;
  }
  .login{
    .login-form{
        background: #fff;
        width: 300px;
        padding: 20px;
        border-radius: 5px
    }
    .login-btn{
        width: 100%
    }
    .denglu{
        font-weight: 1000;
        font-size: 20px;
        padding-bottom: 10px
    }
}
}

</style>

2.接口测试

接口文档:
http://eduboss.lagou.com/boss/doc.html#/home
http://edufront.lagou.com/front/doc.html#/home

3.请求登录

总结:
根据接口要求初始化表单数据,利用postman测试下接口
转化param的格式的转化

<script lang="ts">
import Vue from 'vue'
import request from '@/utils/request'
import qs from 'qs'

export default Vue.extend({
  name: 'LoginIndex',
  data () {
    return {
      form: {
        phone: '',
        password: ''
      }
    }
  },
  methods: {
    async onSubmit () {
      // 1.表单验证
      // 2 验证通过-提交表单
      // 3 处理请求结果
      //    成功-跳转到首页
      //    失败:
      const { data } = await request({
        method: 'POST',
        url: '/front/user/login',
        headers: { 'content-type': 'application/x-www-form-urlencoded' },
        data: qs.stringify(this.form) // axios发送请求,默认发送的是application/json格式的数据,但我们需要的是x-www-form-urlencoded
      })
      console.log(data)
    }
  }
})
</script>

4.处理请求结果

主要处理成功请求跳转首页,并添加消息提示

 const { data } = await request({
        method: 'POST',
        url: '/front/user/login',
        headers: { 'content-type': 'application/x-www-form-urlencoded' },
        data: qs.stringify(this.form) // axios发送请求,默认发送的是application/json格式的数据,但我们需要的是x-www-form-urlencoded
      })
      console.log(data)
      if (data.state !== 1) {
        this.$message.error(data.message)
        return
      }

      this.$router.push({
        name: 'home'
      })
      this.$message.success('登录成功')

5. 表单验证

form 表单验证
具体验证规格:async-validator
注意的问题:
1、表单验证规则
2、表单验证失败,不可提交表单
3、this.$refs[formName].validate()使用时没办法通过Ts校验,原因是this.$refs[formName]没有确定类型。需要将this.$refs[formName]转换类型

await (this.$refs.form as Form).validate()

相关代码:

<template>
  <div class="system">
    <div class="title">Edu boss 管理系统</div>
    <div class="login">
      <!--
        :model= "ruleForm"
        :rules="rules"
        ref="ruleForm"
        4 el-form-item 绑定prop属性
       -->
      <el-form class = "login-form demo-ruleForm" label-position="top"  label-width="80px"
      ref="form"
      :model="form"
      :rules="rules"
       >
        <div class="denglu">登录</div>
        <el-form-item label="手机号" prop="phone">
            <el-input v-model="form.phone"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
            <el-input type="password" v-model="form.password"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit" class="login-btn" :loading="isLoginLoading">登录</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import request from '@/utils/request'
import qs from 'qs'
import { Form } from 'element-ui'

export default Vue.extend({
  name: 'LoginIndex',
  data () {
    return {
      form: {
        phone: '',
        password: ''
      },
      isLoginLoading: false,
      rules: {
        phone: [
          { required: true, message: '请输入手机号', trigger: 'blur' },
          { pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
          { min: 6, max: 18, message: '长度在 6 到 18 个字符', trigger: 'blur' }
        ]
      }
    }
  },
  methods: {
    async onSubmit () {
      try {
        await (this.$refs.form as Form).validate()
        this.isLoginLoading = true
        // 1.表单验证
        // 2 验证通过-提交表单
        // 3 处理请求结果
        //    成功-跳转到首页
        //    失败:
        const { data } = await request({
          method: 'POST',
          url: '/front/user/login',
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
          data: qs.stringify(this.form) // axios发送请求,默认发送的是application/json格式的数据,但我们需要的是x-www-form-urlencoded
        })
        console.log(data)
        if (data.state !== 1) {
          this.$message.error(data.message)
          this.isLoginLoading = false
          return
        }

        this.$router.push({
          name: 'home'
        })
        this.$message.success('登录成功')
        this.isLoginLoading = false
      } catch (err) {
        console.log('登录失败', err)
        this.isLoginLoading = false
      }
    }
  }
})
</script>

<style lang="scss" scoped>
.system{
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: 40px;
  .title{
    display: flex;
    margin-bottom: 50px;
    text-align: center;
    font-size: 45px;
    width: 300px;
  }
  .login{
    .login-form{
        background: #fff;
        width: 300px;
        padding: 20px;
        border-radius: 5px
    }
    .login-btn{
        width: 100%
    }
    .denglu{
        font-weight: 1000;
        font-size: 20px;
        padding-bottom: 10px
    }
}
}
</style>

6. 请求期间禁用按钮点击

网速较慢的情况下,用户可多次点击登录按钮(用户并不知道后端正在登录请求)
可参照加载中
详情请看5中的代码

7. 封装请求方法

services/user.ts

/* 用户相关请求模块 */
import request from '@/utils/request'

import qs from 'qs'

interface User{
  phone: string;
  password: string;
}
export const login = (data: User) => {
  return request({
    method: 'POST',
    url: '/front/user/login',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    data: qs.stringify(data) // axios发送请求,默认发送的是application/json格式的数据,但我们需要的是x-www-form-urlencoded
  })
}

login/index.vue 代码修改

const { data } = await login(this.form)

8.关于请求体data和Contentype的问题

  • 如果 data是普通对象,则 content-type 是application/json
  • 如果 data 是 qs.stringify()转换之后的数据 key=value,则content-type 会被设置为application/x-www-form-urlencoded,则不需要手动设置headers
  • 如果data是FormData对象,则 content-type 是multipart/form-data对象

问题:登录判断,则执行不到this.isLoginLoading = false 部分

if (data.state !== 1) {
     return this.$message.error(data.message)
  }

九 身份认证

1.把登录状态缓存到Vuex容器中

store/index.ts

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  // 容器实现了数据共享,在组件总共享方便,但是没有持久化的功能
  state: {
    user: JSON.parse(window.localStorage.getItem('user') || 'null')
    // user: null // 当前登录用户状态
  },
  mutations: {
    // 修改容器数据必须使用mutations函数
    setUser (state, payload) {
      state.user = JSON.parse(payload)

      // 1为了防止页面刷新,数据丢失,需要把数据持久化
      // 2本地存储只能存储字符串
      window.localStorage.setItem('user', payload)
    }
  },
  actions: {
  },
  modules: {
  }
})

2.校验页面访问权限

路由拦截器的设置。可查看全局前置守卫
给路由配置路由元信息
route/index.ts

import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Layout from '@/layout/index.vue'
import store from '@/store'

Vue.use(VueRouter)

// 路由配置规则
const routes: Array<RouteConfig> = [
  {
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: 'login' */ '@/views/login/index.vue')
  },
  {
    path: '/',
    component: Layout,
    children: [
      {
        path: '', // 默认子路由
        name: 'home',
        component: () => import(/* webpackChunkName: 'home' */ '@/views/home/index.vue'),
        meta: {
          requiresAuth: true // 自定义数据
        } // 默认就是空对象
      },
      {
        path: '/role',
        name: 'role',
        component: () => import(/* webpackChunkName: 'role' */ '@/views/role/index.vue'),
        meta: {
          requiresAuth: true // 自定义数据
        } // 默认就是空对象
      },
      {
        path: '/menu',
        name: 'menu',
        component: () => import(/* webpackChunkName: 'menu' */ '@/views/menu/index.vue'),
        meta: {
          requiresAuth: true // 自定义数据
        } // 默认就是空对象
      },
      {
        path: '/resource',
        name: 'resource',
        component: () => import(/* webpackChunkName: 'resource' */ '@/views/resource/index.vue'),
        meta: {
          requiresAuth: true // 自定义数据
        } // 默认就是空对象
      },
      {
        path: '/course',
        name: 'course',
        component: () => import(/* webpackChunkName: 'course' */ '@/views/course/index.vue'),
        meta: {
          requiresAuth: true // 自定义数据
        } // 默认就是空对象
      },
      {
        path: '/user',
        name: 'user',
        component: () => import(/* webpackChunkName: 'user' */ '@/views/user/index.vue'),
        meta: {
          requiresAuth: true // 自定义数据
        } // 默认就是空对象
      },
      {
        path: '/advert',
        name: 'advert',
        component: () => import(/* webpackChunkName: 'advert' */ '@/views/advert/index.vue'),
        meta: {
          requiresAuth: true // 自定义数据
        } // 默认就是空对象
      },
      {
        path: '/advert-space',
        name: 'advert-space',
        component: () => import(/* webpackChunkName: 'advert-space' */ '@/views/advert-space/index.vue')
      }
    ]
  },
  {
    path: '*',
    name: '404',
    component: () => import(/* webpackChunkName: '404' */ '@/views/error-page/404.vue')
  }

]

const router = new VueRouter({
  routes
})

// 全局前置首位,任何页面的访问都要经过这里
// to:去哪里的路由信息
// from:从哪里来的路由信息
// next:通行的标志
router.beforeEach((to, from, next) => {
  console.log('come in beforeEach')
  console.log('to =>', to)
  console.log('from =>', from)
  // 路由守卫中一定要调用next,否则页面无法访问
  // next()
  // if (to.path !== '/login') {
  //   // 校验登录状态
  // }
  // to.matched匹配到的路由记录,是一个数组
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // this route requires auth, check if logged in
    if (!store.state.user) {
      // 跳转到登录页面
      next({
        name: 'login'
      })
    } else {
      next()
    }
  } else {
    next()
  }
})

export default router

3.测试获取当前登录用户信息接口

4.登录成功跳转回原来页面

route/index.vue修改

if (!store.state.user) {
      // 跳转到登录页面
      next({
        name: 'login',
        query: { // 通过url传递查询字符串参数
          redirect: to.fullPath // 把登录成功需要返回的页面告诉登录页面
        }
      })
    }

login/index.vue中修改

// 2然后在访问需要登录的页面的时候,判断有没有登录状态,(路由拦截器)
  this.$router.push(this.$route.query.redirect as string || '/')

5. 展示当前登录用户信息

services/index.vue

export const getUserInfo = () => {
  return request({
    method: 'GET',
    url: '/front/user/getInfo',
    headers: {
      Authorization: store.state.user.access_token
    }
  })
}

在header加载方法并调用,处理默认头像

import Vue from 'vue'
import { getUserInfo } from '@/services/user'

export default Vue.extend({
  name: 'AppHeader',
  data () {
    return {
      userInfo: {} // 当前用户登录信息
    }
  },
  created () {
    this.loadUserInfo()
  },
  methods: {
    async loadUserInfo () {
      const { data } = await getUserInfo()
      this.userInfo = data.content
    }
  }
})
<span class="el-dropdown-link">
          <el-avatar shape="square" :size="30" :src="userInfo.portrait || require('@/assets/default-avatar.jpg')"></el-avatar>
          <i class="el-icon-arrow-down el-icon--right"></i>
</span>
 <el-dropdown-menu slot="dropdown">
<el-dropdown-item>{{userInfo.userName}}</el-dropdown-item>

6. 使用请求拦截器统一设置Token

自动设置某些请求的token,不用每次请求添加header
请求拦截器
uitls/request.ts中添加

import axios from 'axios'
import store from '@/store'

const request = axios.create({
  // 配置选项
  // baseURL
  // timeout
})
// 请求拦截器
request.interceptors.request.use(function (config) {
  // console.log('come in interceptors', config)
  // 我们就可以在这里通过改写config配置信息来实现业务功能的统一处理

  const { user } = store.state
  if (user && user.access_token) {
    config.headers.Authorization = user.access_token
  }
  // 注意:这里一定要返回config,否则请求就发不出去
  return config
}, function (error) {
  // Do something with request error
  return Promise.reject(error)
})

// 响应拦截器
export default request

添加成功后,可删除
services/user.ts

export const getUserInfo = () => {
  return request({
    method: 'GET',
    url: '/front/user/getInfo'
    // headers: {
    //   Authorization: store.state.user.access_token
    // }
  })
}

7. 用户退出

app-header.vue

 <el-dropdown-item divided
      @click="handleLogout">退出</el-dropdown-item>
      </el-dropdown-menu>

点击事件不可用:原因click是原生dom事件,而这个一个组件,首先看组件上的事件是否支持dom事件,组件上的事件都是自定义事件
思路:1组件是否有可用的事件
2 click 用native方式注册@click.native

app-header.vue

  <el-dropdown-item divided
      @click.native="handleLogout">退出</el-dropdown-item>
      </el-dropdown-menu>
handleLogout () {
      // 清除登录信息状态
      this.$store.commit('setUser', null)
      // 跳转到登录页面
      this.$router.push({
        name: 'login'
      })
    }

为了美观性,可手动添加退出提示 ,参照MessageBox 弹框

handleLogout () {
      this.$confirm('确认退出吗?', '退出提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => { // 确认执行
        // 清除登录信息状态
        this.$store.commit('setUser', null)
        // 跳转到登录页面
        this.$router.push({
          name: 'login'
        })
        this.$message({
          type: 'success',
          message: '退出成功!'
        })
      }).catch(() => {
        this.$message({ // 取消执行
          type: 'info',
          message: '已取消退出'
        })
      })
    }

十 用户登录和身份认证

概念介绍

Token 是后端设置的过期时间
为什么access_token需要有过期时间,以及为什么特写短? 为了安全

  • access_token
    作用:获取需要授权的接口数据
  • expires_in
    作用:设定access_token的过期时间
  • refresh_token
    作用:用来刷新获取新的access_token
  • 解决方法
    • 方法一:
      在请求发起前拦截每个请求,判断Token 的有效时间expires_in是否已经过期,若已经过期,则将请求挂起,先刷新Token后再继续请求

      • 优点:在请求前拦截,能节省请求,省流量
      • 缺点:需要后端额外提供一个Token的过期时间的字段:使用了本地时间判断,若本地时间被篡改,特别是本地时间比服务器时间慢时,拦截会失败
    • 方法二
      不在请求前拦截,而是拦截返回后的数据,先发起请求,接口返回过期后,先刷新Token,在进行一次重试

      • 优点:不需要额外的Token过期时间,不需要判断时间
      • 缺点:会消耗多一次请求,耗流量
    • 总结
      方法一和二有缺点时互补的,方法一游校验失败的风险(本地时间被篡改时,当然一般没有用户去主动修改本地时间),方法二简单粗暴,等知道服务器过期了在重试一次,只是会耗多一个请求

1. 处理过期Token-分析响应拦截器

utils/request.ts添加响应拦截器,添加打印可查看返回的数据

// 响应拦截器
request.interceptors.response.use(function (response) { // 当状态码为2xx,都会进去这里
  console.log('请求响应成功', response)
  // 如果是自定义错误状态码,错误处理就写到这里
  return response
}, function (error) { // 超出2xx,都执行这里
  // 若使用的HTTP错误,则错误处理写到这里
  console.log('请求响应失败', error)
  return Promise.reject(error)
})

2. 处理过期Token-axios错误处理

错误处理示例

// 响应拦截器
request.interceptors.response.use(function (response) { // 当状态码为2xx,都会进去这里
  console.log('请求响应成功', response)
  // 如果是自定义错误状态码,错误处理就写到这里
  return response
}, function (error) { // 超出2xx,都执行这里
  // 若使用的HTTP错误,则错误处理写到这里
  // console.log('请求响应失败', error)
  if (error.response) { // 请求发出去,收到响应了,但是状态码超出了2范围

  } else if (error.request) { // 请求发出去了,但是未收到响应
    // The request was made but no response was received
    // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
    // http.ClientRequest in node.js

  } else { // 在设置请求时发生了一些事情,触发了错误
    // Something happened in setting up the request that triggered an Error

  }

  // 把请求失败的错误对象继续抛出,扔给下一个上一个调用者
  return Promise.reject(error)
})

3. 处理过期Token-错误消息提示

4. 处理过期Token-实现基本流程逻辑

相关代码

import axios from 'axios'
import store from '@/store'
import { Message } from 'element-ui'
import router from '@/router'
import qs from 'qs'

const request = axios.create({
  // 配置选项
  // baseURL
  // timeout
})

function redirectLogin () {
  router.push({
    name: 'login',
    query: {
      redirect: router.currentRoute.fullPath
    }
  })
}

function refreshToken () {
  return axios.create()({
    method: 'POST',
    // 同一个refresh_token,该接口只能使用一次!!!否者会报错
    url: '/front/user/refresh_token',
    data: qs.stringify({
      refreshtoken: store.state.user.refresh_token
    })
  })
}

// 请求拦截器
request.interceptors.request.use(function (config) {
  // console.log('come in interceptors', config)
  // 我们就可以在这里通过改写config配置信息来实现业务功能的统一处理

  const { user } = store.state
  if (user && user.access_token) {
    config.headers.Authorization = user.access_token
  }
  // 注意:这里一定要返回config,否则请求就发不出去
  return config
}, function (error) {
  // Do something with request error
  return Promise.reject(error)
})

// 响应拦截器
request.interceptors.response.use(function (response) { // 当状态码为2xx,都会进去这里
  console.log('请求响应成功', response)
  // 如果是自定义错误状态码,错误处理就写到这里
  return response
}, async function (error) { // 超出2xx,都执行这里
  // 若使用的HTTP错误,则错误处理写到这里
  // console.log('请求响应失败', error)
  if (error.response) { // 请求发出去,收到响应了,但是状态码超出了2范围
    const { status } = error.response
    // 400
    // 401
    // 403
    // 404
    // 500
    if (status === 400) {
      Message.error('请求参数错误')
    } else if (status === 401) {
      // Token无效(Token过期,或者未提供Token)
      // 如果有refresh_token,则尝试使用refresh_token更新access_token
      if (!store.state.user) {
        console.log('come in store.state.user redirectLogin')
        redirectLogin()
        return Promise.reject(error)
      }

      // 尝试刷新新的cookie
      try {
        console.log('come in store.state.user redirectLogin22')
        // request({ 不是用的原因,refreshtoken刷新接口,也有可能会出现401的错误,这样会陷入循环中
        const { data } = await axios.create()({ // 新的axios对象,未有拦截器
          method: 'POST',
          url: '/front/user/refresh_token',
          data: qs.stringify({
            refreshtoken: store.state.user.refresh_token
          })
        })
        // 成功了=> 则把本次失败的请求重新发出去
        // 把刷新拿到的新access_token,更新到容器中和本地存储
        store.commit('setUser', data.content)
        return request(error.config)
      } catch (error) {
        // 把当前登录用户状态清除
        store.commit('setUser', null)
        // 失败了=> 跳转登录页重新登录获取新的token
        redirectLogin()
        return Promise.reject(error)
      }
      // Message.error('')
    } else if (status === 403) {
      Message.error('没有权限,请联系管理员')
    } else if (status === 404) {
      Message.error('请求资源不存在')
    } else if (status >= 500) {
      Message.error('服务端错误,请联系管理员')
    }
  } else if (error.request) { // 请求发出去了,但是未收到响应
    // The request was made but no response was received
    // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
    // http.ClientRequest in node.js
    Message.error('请求超时,请刷新重试')
  } else { // 在设置请求时发生了一些事情,触发了错误
    // Something happened in setting up the request that triggered an Error
    Message.error(`请求失败:${error.message}`)
  }

  // 把请求失败的错误对象继续抛出,扔给下一个上一个调用者
  return Promise.reject(error)
})

export default request

5. 处理过期Token-过于多次请求的问题

同意时间多个token过期

6. 处理过期Token-解决多次请求刷新Token的问题

7. 处理过期Token-解决多次请求其他接口重试问题

思路:定义数组,将刷新期间失败的请求放到数组中,被挂起,返回promise(因为可控制完成状态)

在书信token成功之后,调用requests数组中的内容

import axios from 'axios'
import store from '@/store'
import { Message } from 'element-ui'
import router from '@/router'
import qs from 'qs'

const request = axios.create({
  // 配置选项
  // baseURL
  // timeout
})

function redirectLogin () {
  router.push({
    name: 'login',
    query: {
      redirect: router.currentRoute.fullPath
    }
  })
}

function refreshToken () {
  return axios.create()({
    method: 'POST',
    // 同一个refresh_token,该接口只能使用一次!!!否者会报错
    url: '/front/user/refresh_token',
    data: qs.stringify({
      refreshtoken: store.state.user.refresh_token
    })
  })
}

// 请求拦截器
request.interceptors.request.use(function (config) {
  // console.log('come in interceptors', config)
  // 我们就可以在这里通过改写config配置信息来实现业务功能的统一处理

  const { user } = store.state
  if (user && user.access_token) {
    config.headers.Authorization = user.access_token
  }
  // 注意:这里一定要返回config,否则请求就发不出去
  return config
}, function (error) {
  // Do something with request error
  return Promise.reject(error)
})

// 响应拦截器
let isRefreshing = false // 刷新token状态
let requests: any [] = []// 刷新token状态期间被挂起的请求

request.interceptors.response.use(function (response) { // 当状态码为2xx,都会进去这里
  console.log('请求响应成功', response)
  // 如果是自定义错误状态码,错误处理就写到这里
  return response
}, async function (error) { // 超出2xx,都执行这里
  // 若使用的HTTP错误,则错误处理写到这里
  // console.log('请求响应失败', error)
  if (error.response) { // 请求发出去,收到响应了,但是状态码超出了2范围
    const { status } = error.response
    // 400
    // 401
    // 403
    // 404
    // 500
    if (status === 400) {
      Message.error('请求参数错误')
    } else if (status === 401) {
      // Token无效(Token过期,或者未提供Token)
      // 如果有refresh_token,则尝试使用refresh_token更新access_token
      if (!store.state.user) {
        console.log('come in store.state.user redirectLogin')
        redirectLogin()
        return Promise.reject(error)
      }

      if (!isRefreshing) {
        isRefreshing = true
        // 尝试刷新新的cookie
        return refreshToken().then(res => {
          if (!res.data.success) {
            throw new Error('刷新 token 失败')
          }
          store.commit('setUser', res.data.content)

          // 刷新 toekn 成功, 就讲所有挂起的请求执行掉
          requests.forEach(cb => cb())
          // 将执行过的 请求数组 清空
          requests = []

          return request(error.config)
        }).catch(err => {
          console.log(err)
          // 把当前登录用户状态清除
          store.commit('setUser', null)
          // 失败了=> 跳转登录页重新登录获取新的token
          redirectLogin()
          return Promise.reject(error)
        }).finally(() => {
          isRefreshing = false
        })
      }
      // 刷新状态下,把请求挂起,放到requests数组中
      return new Promise(resolve => {
        requests.push(() => {
          resolve(request(error.config))
        })
      })
      // Message.error('')
    } else if (status === 403) {
      Message.error('没有权限,请联系管理员')
    } else if (status === 404) {
      Message.error('请求资源不存在')
    } else if (status >= 500) {
      Message.error('服务端错误,请联系管理员')
    }
  } else if (error.request) { // 请求发出去了,但是未收到响应
    // The request was made but no response was received
    // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
    // http.ClientRequest in node.js
    Message.error('请求超时,请刷新重试')
  } else { // 在设置请求时发生了一些事情,触发了错误
    // Something happened in setting up the request that triggered an Error
    Message.error(`请求失败:${error.message}`)
  }

  // 把请求失败的错误对象继续抛出,扔给下一个上一个调用者
  return Promise.reject(error)
})

export default request

十一 用户权限

介绍

作用:管理用户的权限,

  • 可配置不同的菜单展示
  • 资源的限制

1. 菜单管理-添加菜单-布局

在这里插入图片描述
页面布局处理(可参照Card 卡片)

  • 添加菜单
  • 列表

十二 角色权限管理

十三 课程管理

富文本编辑器的使用:

十四 编辑课程

十五 发布部署

1. 发布部署-项目打包

  • npm run build
  • npm install -g serve
  • serve -s dist

2. 发布部署-本地预览服务

解决本地调试,接口代理问题

  • 创建目录:
    test-serve 中,添加app.js
  • 安装express ,安装到开发依赖中
    npm i -D express

中间件https://github.com/chimurai/http-proxy-middleware

  • 安装npm install --save-dev http-proxy-middleware
    文件内容
const express = require('express')
const path = require('path')
const app = express()

const { createProxyMiddleware } = require('http-proxy-middleware')

// 托管了 dist 目录,当访问 / 的时候,默认会返回托管目录中的 index.html 文件
app.use(express.static(path.join(__dirname, '../dist')))


app.use('/boss', createProxyMiddleware({
  target: 'http://eduboss.lagou.com',
  changeOrigin: true
}))

app.use('/front', createProxyMiddleware({
  target: 'http://edufront.lagou.com',
  changeOrigin: true
}))

app.use
app.listen(3000,()=>{
  console.log('running')
})

3. 发布部署-注意事项

4. 发布部署-部署说明

总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值