pc端面经

154 阅读7分钟

项目演示: 本地源代码《教学资料》

接口文档: www.apifox.cn/apidoc/proj…

接口根路径: interview-api-t.itheima.net/

本项目的技术栈 本项目技术栈基于 ES2015+vue2vuex3vue-router3vue-cli5axioselement-ui

image.png

2.sass/scss 语法说明

less sass stylus 都是 css 预处理器,语法上稍有差异,作用一样
都是让 css,增强能力,具备变量,函数.. 的能力

sass的语法两种语法 .sass(旧) .scss(新)
1 .sass 和 .stylus 语法很像 (了解)
  要求省略 {} 和 分号, 缩进表示嵌套
  
2 .scss 和 .less   语法很像, 都支持嵌套, 变量...
  scss 声明变量:$变量名
  less 声明变量: @变量名

三、调整项目目录

默认生成的目录结构不满足我们的开发需求,所以这里需要做一些自定义改动。主要是两个工作:

  • 删除初始化的默认文件
  • 修改剩余代码内容
  • 新增调整我们需要的目录结构

1.删除文件

  • components/HelloWorld.vue
  • views/HomeView.vue
  • views/AboutView.vue
  • assets/logo.png

2.修改内容

src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: []
})

export default router

src/App.vue

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<style lang="scss">

</style>

store/index.js 和 main.js 不用动

3.新增需要目录

在 src 目录下中补充创建以下目录:

  • /api : 存储请求函数模块
  • /styles: 样式文件模块
  • /utils: 工具函数模块

image.png

  1. 将项目需要的图片资源放置 assets 文件夹

四、引入 element-ui 组件库

官方文档: element.eleme.io/#/zh-CN

1.全部引入

全部引入, 会导入所有的组件, 但是体积会变大

  • 安装
yarn add element-ui
  • main.js
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
  • 演示
<el-button type="primary">主要按钮</el-button>

2.按需导入 (推荐)

减轻将来打包后的包的体积

  • 安装
yarn add element-ui
  • 安装babel-plugin-component
yarn add babel-plugin-component -D
  • babel.config.js中配置
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  // 新增plugins插件节点,修改完配置文件一定重启项目
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}
  • 使用插件main.js
import { Button } from 'element-ui'
Vue.use(Button)

3.抽离element.js模块

  • 由于组件的导入都书写到了main.js中,导致main.js 代码冗余

    将element-ui组件的导入和注册单独抽离到utils文件夹中

  • 新建element.js

  • **项目中 ** 完整按需导入如下:

import Vue from 'vue';
import {
  Popconfirm,
  Avatar,
  Breadcrumb,
  BreadcrumbItem,
  Pagination,
  Dialog,
  Menu,
  Input,
  Option,
  Button,
  Table,
  TableColumn,
  Form,
  FormItem,
  Icon,
  Row,
  Col,
  Card,
  Container,
  Header,
  Aside,
  Main,
  Footer,
  Link,
  Image,
  Loading,
  MessageBox,
  Message,
  Drawer,
  MenuItem
} from 'element-ui';

Vue.use(Breadcrumb);
Vue.use(BreadcrumbItem);
Vue.use(Drawer);
Vue.use(Popconfirm);
Vue.use(Avatar);
Vue.use(Pagination);
Vue.use(Dialog);
Vue.use(Menu);
Vue.use(MenuItem);
Vue.use(Input);
Vue.use(Option);
Vue.use(Button);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Icon);
Vue.use(Row);
Vue.use(Col);
Vue.use(Card);
Vue.use(Container);
Vue.use(Header);
Vue.use(Aside);
Vue.use(Main);
Vue.use(Footer);
Vue.use(Link);
Vue.use(Image);

Vue.use(Loading.directive);

Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;
  • 直接导入main.js中
// 直接导入vant-ui.js
import '@/utils/element.js'

4.主题色定制

官网

新建 styles/index.scss

// 修改主题色
$--color-primary: rgba(114,124,245,1);
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";

//初始化body样式
body {
  margin: 0;
  padding: 0;
  background: #fafbfe;
}

main.js 引入

import '@/styles/index.scss'

五、公共模块的封装

1.request模块 - axios封装

接口文档地址:www.apifox.cn/apidoc/proj…

我们会使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如: 配置基础地址等)

一般项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个模块中, 便于使用

  1. 安装 axios
npm i axios
  1. 新建 utils/request.js 封装 axios 模块

    利用 axios.create 创建一个自定义的 axios 来使用

    www.axios-js.com/zh-cn/docs/…

/* 封装axios用于发送请求 */
import axios from 'axios'

// 创建一个新的axios实例
const request = axios.create({
  baseURL: 'http://interview-api-t.itheima.net/',
  timeout: 5000
})

// 添加请求拦截器
request.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

// 添加响应拦截器
request.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response.data
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error)
})

export default request

2.storage模块 - 本地存储

新建 utils/storage.js

// 以前 token 令牌,如果存到了本地,每一次都写这么长,太麻烦
// localStorage.setItem(键, 值)
// localStorage.getItem(键)
// localStorage.removeItem(键)

const KEY = 'my-token-element-pc'

// 直接用按需导出,可以导出多个
// 但是按需导出,导入时必须 import { getToken } from '模块名导入'

// 获取
export const getToken = () => {
  return localStorage.getItem(KEY)
}

// 设置
export const setToken = (newToken) => {
  localStorage.setItem(KEY, newToken)
}

// 删除
export const delToken = () => {
  localStorage.removeItem(KEY)
}

六、路由设计配置

但凡是: 单个页面,独立展示的,都是一级路由 (登录 注册 首页架子 文章详情 ...)

image.png

路由设计:

  • 登录页 (一级) login
  • 首页架子(一级) layout
    • 数据看板(二级)dashboard
    • 文章管理(二级)article

1.新建目录

image.png

以上文件夹及文件 直接从【素材中】拷贝即可

2.配置路由

router/index.js

import VueRouter from 'vue-router'
import Vue from 'vue'

import Layout from '@/views/layout'
import Login from '@/views/login'
import Dashboard from '@/views/dashboard'
import Article from '@/views/article'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    { path: '/login', component: Login },
    {
      path: '/',
      component: Layout,
      redirect: '/dashboard',
      children: [
        { path: 'dashboard', component: Dashboard },
        { path: 'article', component: Article }
      ]
    }
  ]
})

export default router

layout/index 配置二级路由出口

<template>
  <div>
    <div>头部</div>
    <div>侧边</div>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'LayoutIndex'
}
</script>

<style>

</style>

测试路径1: http://localhost:8080/#/login

测试路径2: http://localhost:8080/#/dashboard

测试路径3: http://localhost:8080/#/article

七、登录模块

1.说明:

我们先学习 element-ui 表单组件的基本结构使用

2.需求:

实现如图效果

image.png

3.样式控制

一般情况,这种第三方的组件,为了样式控制方便,会给组件的根元素,起一个和组件名同名的类名

控制组件的样式:

  1. 直接通过组件名 同名的 类, 进行控制样式

  2. 自己通过添加 class 类名,进行控制样式

默认,写在scoped中的样式,只会影响到当前组件模板中的元素内容

深度作用选择器:向下影响到子元素的样式

::v-deep (scss)

/deep/ (less)

<template>
  <div class="login-page">
    <el-card class="el-card">
      <template #header>黑马面经运营后台</template>
      <el-form>
        <el-form-item label="用户名:">
          <el-input placeholder="请输入用户名" />
        </el-form-item>
        <el-form-item label="密码:">
          <el-input placeholder="请输入密码:" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary">登录</el-button>
          <el-button>重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'login-page'
}
</script>

<style lang="scss" scoped>
.el-card {
  width: 420px;
  margin: 0 auto;
  // 深度作用选择器   ::v-deep   /deep/
  ::v-deep .el-card__header {
    background: rgba(114,124,245,1);
    text-align: center;
    color: white;
  }
}
</style>

八、登录模块-样式美化

image.png

<template>
  <div class="login-page">
    <el-card>
      <template #header>黑马面经运营后台</template>
      <el-form autocomplete="off">
        <el-form-item label="用户名">
          <el-input placeholder="输入用户名"></el-input>
        </el-form-item>

        <el-form-item label="密码">
          <el-input type="password" placeholder="输入用户密码"></el-input>
        </el-form-item>

        <el-form-item class="tc">
          <el-button type="primary">登 录</el-button>
          <el-button >重 置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'login-page',
  data () {
    return {

    }
  },
  methods: {

  }
}
</script>

<style lang="scss" scoped>
.login-page {
  min-height: 100vh;
  background: url(@/assets/login-bg.svg) no-repeat center / cover;
  display: flex;
  align-items: center;
  justify-content: space-around;
  .el-card {
    width: 420px;
    ::v-deep .el-card__header{
      height: 80px;
      background: rgba(114,124,245,1);
      text-align: center;
      line-height: 40px;
      color: #fff;
      font-size: 18px;
    }
  }
  .el-form {
    padding: 0 20px;
  }
  .tc {
    text-align: center;
  }
}
</style>

九、element-ui 基本校验

说明:在向后端发请求,调用接口之前,我们需要对所要传递的参数进行验证,把用户的错误扼杀在摇篮之中。

讲解内容:

  • element-ui的校验

    • el-form: model属性, rules规则

    • el-form-item: 绑定 prop 属性

    • el-input: 绑定 v-model

Form 组件提供了表单验证的功能

  1. form组件需要 :model绑定form对象(必须), 需要通过 rules 属性传入约定的验证规则
<el-form :model="form" :rules="rules">
    
export default {
  data() {
    return {
      form: {
        username: '',
        password: ''
      }
    }
  }
}
  1. 在 data 中准备 rules 规则
rules: {
  username: [
    { required: true, message: '请输入用户名', trigger: ['blur', 'change'] },
    { min: 5, max: 11, message: '长度在 5 到 11 个字符', trigger: ['blur', 'change'] }
  ]
}
  1. 将 Form-Item 的 prop 属性设置为需校验的字段名
<el-form-item label="用户名:" prop="username">
  <el-input v-model="form.username" placeholder="请输入手机号" />
</el-form-item>

十、element-ui 正则校验

下面是常用内置的基本验证规则:其余校验规则参见 async-validator

规则说明
required必须的,例如校验内容是否非空
pattern正则表达式,例如校验手机号码格式、校验邮箱格式
rules: {
  username: [
    { required: true, message: '请输入用户名', trigger: ['blur', 'change'] },
    { min: 5, max: 11, message: '长度在 5 到 11 个字符', trigger: ['blur', 'change'] }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: ['blur', 'change'] },
    { pattern: /^\w{5,11}$/, message: '请输入 5 到 10 位的密码', trigger: ['blur', 'change'] }
  ]
}

// \d 数字 0-9
// \w 字母数字下划线
// {m,n} 前面的字符,可以出现 m次 ~ n次

不要忘了配置prop

<el-form-item prop="password">

上述已经可以完成大部分需求,如果需要更复杂业务校验需求,可以自定义校验~ (项目课程:人力资源系统会进一步讲解)

十一、提交表单校验 和 重置

每次点击按钮, 进行ajax登录前, 应该先对整个表单内容校验, 不然还是会发送很多无效的请求!!!

要通过校验了, 才发送请求!!!

作用: ref 属性配合 $refs 可以获取 dom 元素 (或者 vue组件实例)

  1. 给组件或者元素, 添加 ref 属性
<hello ref="bb"></hello>
  1. 通过 this.$refs 可以获取对应的引用, 并且调用方法
this.$refs.bb.sayHi()

添加登录提交的校验

<el-form ref="form" :model="form" :rules="rules" autocomplete="off">
...
<el-button @click="login" type="primary">登 录</el-button>

methods: {
  login () {
      this.$refs.form.validate(valid =>{
          if(!valid){
              return
          }
          console.log('可以发送请求了')
      })   
  }
}

添加重置功能

<el-button @click="reset">重 置</el-button>

methods: {
  reset () {
    this.$refs.form.resetFields()
  }
}

十二、封装登录api登录请求

新建 api/user.js 提供api接口函数

import request from '@/utils/request'

export const login = ({ username, password }) => {
  return request.post('/auth/login', {
    username,
    password
  })
}

发送请求获取token

methods: {
   login () {
       this.$refs.form.validate(async valid =>{
        if(!valid){
            return
        }
  		const res= await login(this.form)
        console.log(res)
      })    
  }
}

十三、vuex - user 模块 - 存token

image.png

新建 store/modules/user.js

import { getToken, setToken } from '@/utils/storage'

export default {
  namespaced: true,
  state () {
    return {
      token: getToken()
    }
  },
  mutations: {
    setUserToken (state, payload) {
      state.token = payload
      setToken(payload)
    }
  },
  actions: { 
    async loginAction (context, obj) {
      // 发送登录请求
      const res = await login(obj)
      // commit mutation
      context.commit('setUserToken', res.data.token)
    }
  },
}

挂载模块

import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    user
  }
})

登录时调用

 login () {
     this.$refs.form.validate(async valid => {
         if(!valid) return  
         await this.$store.dispatch('user/setUserToken', this.form)
     })    
},

十四、统一错误拦截

import { Message } from 'element-ui'


// 添加响应拦截器
request.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response.data
}, function (error) {
  // 所有的响应错误信息,统一处理
  if (error.response) { 
      Message.error(error.response.data.message) 
  }
  // 对响应错误做点什么
  return Promise.reject(error)
})

十五、登录访问拦截

router/index.js

没有token 且 访问的不是 登录页,就直接拦截到登录

// 白名单,定义成登录
const whiteList = ['/login']

// 路由导航守卫
router.beforeEach((to, from, next) => {
  // 1. 看有没有 token (vuex),如果有,直接放行
  const token = store.state.user.token
  if (token) {
    next()
    return
  }

  // 2. 看是否在 白名单,如果在,直接放行
  if (whiteList.includes(to.path)) {
    next()
    return
  }

  // 3. 其他情况,拦截到登录
  next('/login')
})

十六、首页 layout 模块及请求拦截器统一处理

image.png

1.layout 布局

api/user.js 准备api接口

export const getUser = () => {
  return request.get('/auth/currentUser')
}

layout/index.vue准备结构 (已准备)

<template>
  <el-container class="layout-page">
    <el-aside width="200px">
      <div class="logo">黑马面经</div>
      <el-menu
        router
        :default-active="$route.path"
        background-color="#313a46"
        text-color="#8391a2"
        active-text-color="#FFF"
      >
        <el-menu-item index="/dashboard">
          <i class="el-icon-pie-chart"></i>
          <span>数据看板</span>
        </el-menu-item>
        <el-menu-item index="/article">
          <i class="el-icon-notebook-1"></i>
          <span>面经管理</span>
        </el-menu-item>
      </el-menu>
    </el-aside>
    <el-container>
      <el-header>
        <div class="user">
          <el-avatar
            :size="36"
            :src="avatar"
          ></el-avatar>
          <el-link :underline="false">{{name}}</el-link>
        </div>
        <div class="logout">
          <el-popconfirm title="您确认退出黑马面运营后台吗?" @confirm="handleConfirm">
            <i slot="reference" title="logout" class="el-icon-switch-button"></i>
          </el-popconfirm>
        </div>
      </el-header>
      <el-main>
        <router-view></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>

<script>
import { getUser } from '@/api/user'
export default {
  name: 'layout-page',
  data () {
    return {
      avatar: '',
      name: ''
    }
  },
  created () {
    this.initData()
  },
  methods: {
    async initData () {
      const { data } = await getUser()
      this.avatar = data.avatar
      this.name = data.name
    },
    handleConfirm () {
      this.$router.push('/login')
    }
  }
}
</script>

<style lang="scss" scoped>
.layout-page {
  height: 100vh;
  .el-aside {
    background: #313a46;
    .logo {
      color: #fff;
      font-size: 20px;
      height: 60px;
      line-height: 60px;
      text-align: center;
    }
    .el-menu {
      border-right: none;
      margin-top: 20px;
      &-item {
        background-color: transparent !important;
        > span, i {
          padding-left: 5px;
        }
      }
    }
  }
  .el-header {
    box-shadow: 0px 0px 35px 0px rgba(154, 161, 171, 0.15);
    background: #fff;
    display: flex;
    justify-content: flex-end;
    align-items: center;
    z-index: 999;
    .user {
      display: flex;
      align-items: center;
      background: #fafbfd;
      height: 60px;
      border: 1px solid #f1f3fa;
      padding: 0 15px;
      .el-avatar {
        margin-right: 15px;
      }
    }
    .logout {
      font-size: 20px;
      color: #999;
      cursor: pointer;
      padding: 0 15px;
    }
  }
  .el-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    color: #aaa;
    border-top: 1px solid rgba(152, 166, 173, 0.2);
    font-size: 14px;
  }
}
</style>

遇到 401 错误

image.png

2.请求拦截器携带token

utils/request.js

// 添加请求拦截器
request.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  const { token } = store.state.user
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

十七、退出功能

退出操作

handleConfirm () {
  // this.$router.push('/login')
  this.$store.commit('user/logout')
  this.$router.push('/login')
}

提供mutation

import { delToken, getToken, setToken } from '@/utils/storage'

export default {
  namespaced: true,
  state () {
    return {
      token: getToken()||''
    }
  },
  mutations: {
    ...,
    logout (state) {
      state.token = ''
      delToken()
    }
  }
}

十八、处理token过期

image.png

响应拦截器,处理token过期

import router from '../router'

// 添加响应拦截器
request.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response.data
}, function (error) {
  // 对响应错误做点什么  普通错误 + 401情况
  // console.dir(error)
  if (error.response) {
    if (error.response.status === 401) {
      // 给提示,清除无效token(vuex+本地),拦到登录
      Message.error('尊敬的用户,当前登录状态已过期!')

      // 提交清除token的mutation
      store.commit('user/logout')

      // 跳转到登录
      router.push('/login')
    } else {
      // 给提示
      Message.error(error.response.data.message)
    }
  }
  return Promise.reject(error)
})

一、明确面经管理基本需求

image.png

二、渲染功能-准备架子和认识表格

artcile/index.vue

<template>
  <div class="article-page">
    <el-breadcrumb separator-class="el-icon-arrow-right">
      <el-breadcrumb-item>面经后台</el-breadcrumb-item>
      <el-breadcrumb-item>面经管理</el-breadcrumb-item>
    </el-breadcrumb>
    <el-card shadow="never" border="false">
      <template #header>
        <div class="header">
          <span>共 300 条记录</span>
          <el-button
            icon="el-icon-plus"
            size="small"
            type="primary"
            round>
            添加面经
          </el-button>
        </div>
      </template>

    </el-card>
  </div>
</template>

<script>
export default {
  name: 'article-page',
  data () {
    return {}
  },
  created () {
  },
  methods: {
  }
}
</script>

<style lang="scss" scoped>
.el-card {
  margin-top: 25px;
  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-right: 16px;
  }
  .actions {
    font-size: 18px;
    display: flex;
    justify-content: space-around;
    color: #666;
    > i:hover {
      color: rgba(114, 124, 245, 1);
      cursor: pointer;
    }
  }
}
.el-pagination {
  margin-top: 20px;
  text-align: center;
}
.el-breadcrumb {
  margin-top: 10px;
}
.el-form {
  padding-right: 40px;
}
.quill-editor {
  ::v-deep .ql-editor {
    height: 300px;
  }
}
.el-rate {
  padding: 10px 0;
}
.article-preview {
  padding: 0 40px 40px 40px;
  > h5 {
    font-size: 20px;
    color: #666;
    border-bottom: 1px dashed #ccc;
    padding-bottom: 30px;
    margin: 0 0 20px 0;
  }
}
</style>

表格基本 属性解读

image.png

  • data 数据源
  • prop 设置数据源中对象中的键名,即可填入数据
  • label 列名
  • width 列宽
<el-table :data="tableData" stripe style="width: 100%">
  <el-table-column prop="date" label="日期" width="180">
  </el-table-column>
  <el-table-column prop="name" label="姓名" width="180">
  </el-table-column>
  <el-table-column prop="address" label="地址"> </el-table-column>
</el-table>

data () {
  return {
    tableData: [{
      date: '2016-05-02',
      name: '王小虎',
      address: '上海市普陀区金沙江路 1518 弄'
    }, {
      date: '2016-05-04',
      name: '王小虎',
      address: '上海市普陀区金沙江路 1517 弄'
    }, {
      date: '2016-05-01',
      name: '王小虎',
      address: '上海市普陀区金沙江路 1519 弄'
    }, {
      date: '2016-05-03',
      name: '王小虎',
      address: '上海市普陀区金沙江路 1516 弄'
    }]
  }
},

三、渲染功能-封装接口

新建 api/article.js

import request from '@/utils/request'

export const getArticleList = data => {
  return request.get('/admin/interview/query', {
    params: data
  })
}

article/index.vue created中发送初始化获取数据的请求

data () {
  return {
    current: 1,
    pageSize: 10,
    total: 0,
    list: []
  }
},
created () {
  this.initData()
},
methods: {
  async initData () {
    const { data } = await getArticleList({
      current: this.current,
      pageSize: this.pageSize
    })
    this.list = data.rows
    this.total = data.total
    console.log(data)
  }
}

四、渲染功能-动态渲染表格

<!-- el-table-column 列组件:
 prop:配置数据中的属性名
 label:表格的列名
 width:表格的宽度 -->
<el-table :data="list" style="width: 100%">
  <el-table-column prop="stem" label="标题" width="400">
  </el-table-column>
  <el-table-column prop="creator" label="作者"> </el-table-column>
  <el-table-column prop="likeCount" label="点赞"> </el-table-column>
  <el-table-column prop="views" label="浏览数"> </el-table-column>
  <el-table-column prop="createdAt" label="更新时间" width="200">
  </el-table-column>
</el-table>

五、渲染功能-操作按钮部分

1.说明:列的渲染的两种方式

1.prop 渲染

2.作用域插槽渲染 (自定义列)

2.代码实现:

<el-table :data="list" style="width: 100%">
  <el-table-column prop="stem" label="标题" width="400">
  </el-table-column>
  <el-table-column prop="creator" label="作者"> </el-table-column>
  <el-table-column prop="likeCount" label="点赞"> </el-table-column>
  <el-table-column prop="views" label="浏览数"> </el-table-column>
  <el-table-column prop="createdAt" label="更新时间" width="200">
  </el-table-column>
  <el-table-column label="操作" width="120px">
    <template #default="{ row }">
      <div class="actions">
        <i class="el-icon-view"></i>
        <i class="el-icon-edit-outline"></i>
        <i class="el-icon-delete" @click="del(row.id)"></i>
      </div>
    </template>
  </el-table-column>
</el-table>


del (id) {
  console.log(id)
}

六、渲染功能-基本分页渲染

1.讲解el-pagination完整版的配置项

2.根据完整版的配置项,实现业务功能

<el-pagination
  background
  @current-change="handleCurrentChange"
  :current-page="current"
  :page-size="pageSize"
  layout="prev, pager, next"
  :total="total"
>
</el-pagination>


handleCurrentChange (val) {
  // 处理当前页变化
  this.current = val
  this.initData()
}

七、点击添加、预览、编辑共用逻辑

添加 预览 修改,都要打开抽屉,可以复用

1.三个按钮点击时都要打开抽屉

2.预览和编辑时要获取id

<el-button @click="openDrawer('add')" icon="el-icon-plus" size="small" type="primary" round>
  添加面经
</el-button>

<el-table-column label="操作" width="120px">
  <template #default="{ row }">
    <div class="actions">
      <i class="el-icon-view" @click="openDrawer('preview', row.id, )"></i>
      <i class="el-icon-edit-outline" @click="openDrawer('edit', row.id)"></i>
      <i class="el-icon-delete" @click="del(row.id)"></i>
    </div>
  </template>
</el-table-column>

openDrawer (type, id) {
  console.log(type, id)
}

八、打开抽屉 - 显示抽屉

<!-- 抽屉区域
         1. title="我是标题"
         2. :visible 控制显示隐藏
         3. :direction="direction"  控制方向
         4. :before-close="handleClose" 关闭抽屉前的处理逻辑 (比如:询问客户是否真的要关闭?)
         5. size="60%" 窗体所占的区域多宽
-->

<el-drawer
  :visible.sync="isShowDrawer"
  :before-close="handleClose"
  title="大标题"
  direction="rtl"
  size="60%"
>
  <span>我来啦!</span>
</el-drawer>

data () {
  return {
    current: 1,
    pageSize: 10,
    total: 0,
    list: [],
    isShowDrawer: false
  }
},


openDrawer (type, id) {
  console.log(type, id)
  this.isShowDrawer = true
},

handleClose (done) {
 // $confirm 可以弹出一个确认框,可确认.then  可取消.catch
      this.$confirm('你确认要关闭么?').then(() => {
        done() // done() 调用,就代表关闭抽屉
      }).catch((e) => {
        console.log('取消', e)
      })
}

九、计算属性控制标题

image.png

每个抽屉的title 都是不一样的,可以通过记录drawerType, 提供计算属性,计算出每个title的值

data () {
  return {
    current: 1,
    pageSize: 10,
    total: 0,
    list: [],
    isShowDrawer: false,
    drawerType: ''
  }
},
methods: {  
    openDrawer (type, id) {
      // console.log(type, id)
      this.drawerType = type
      this.isShowDrawer = true
    },
},
    
computed: {
  drawerTitle () {
    let title = '大标题'
    if (this.drawerType === 'add') title = '添加面经'
    if (this.drawerType === 'preview') title = '面经预览'
    if (this.drawerType === 'edit') title = '修改面经'
    return title
  }
},

结构中渲染

<el-drawer
  :visible.sync="isShowDrawer"
  :before-close="handleClose"
  :title="drawerTitle"
  direction="rtl"
>
  <span>我来啦!</span>
</el-drawer>

十、准备表单结构

image.png

<el-form ref="form" label-width="80px">
  <el-form-item label="标题">
    <el-input  placeholder="输入面经标题"></el-input>
  </el-form-item>
  <el-form-item label="内容">
    富文本编辑器
  </el-form-item>
  <el-form-item>
    <el-button type="primary">确认</el-button>
    <el-button>取消</el-button>
  </el-form-item>
</el-form>

十一、富文本编辑器

www.npmjs.com/package/vue…

装包

npm install vue-quill-editor

导入, 局部注册

// require styles
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
 
import { quillEditor } from 'vue-quill-editor'
 
export default {
  components: {
    quillEditor
  }
}

使用, v-model 绑定数据

<el-form ref="form" label-width="80px">
  <el-form-item label="标题" prop="stem">
    <el-input v-model="form.stem" placeholder="输入面经标题"></el-input>
  </el-form-item>
  <el-form-item label="内容" prop="content">
    <quill-editor v-model="form.content"></quill-editor>
  </el-form-item>
  <el-form-item>
    <el-button type="primary">确认</el-button>
    <el-button>取消</el-button>
  </el-form-item>
</el-form>

data(){
    return {
      form: {
        stem: '', // 标题
        content: '' // 内容
      },
    }
}

十二、添加非空校验

1.目标

实现表单的非空校验

2.说明

element-ui 自带的校验,对element-ui表单组件才会默认校验,其他组件标签,需要手动校验

3.代码说明:

3.1element-ui 表单组件的校验

​ el-form组件 → :model="form对象" :rules="rules规则"

​ el-form-item组件 → prop 字段

​ el-input组件 → v-model

3.2富文本编辑器的手动校验

image.png

<el-form :model="form" :rules="rules" ref="form" label-width="80px">
  <el-form-item label="标题" prop="stem">
    <el-input v-model="form.stem" placeholder="输入面经标题"></el-input>
  </el-form-item>
  <el-form-item label="内容" prop="content">
    <quill-editor v-model="form.content"></quill-editor>
  </el-form-item>
  <el-form-item>
    <el-button type="primary" @click="submit">确认</el-button>
    <el-button>取消</el-button>
  </el-form-item>
</el-form>

rules: {
  stem: [{ required: true, message: '请输入面经标题', trigger: 'blur' }],
  content: [{ required: true, message: '请输入面经标题', trigger: 'blur' }]
}

富文本编辑器,校验单独处理

<quill-editor v-model="form.content" @blur="$refs.form.validateField('content')"></quill-editor>

十三、封装接口 - 添加完成

1.实现思路

image.png

2.代码实现

api/article.js

export const createArticle = data => {
  return request.post('/admin/interview/create', data)
}

发送请求添加-完成添加

async submit () {
  try {
    // 校验 (对整个表单校验)
    await this.$refs.form.validate()
    // 请求
    await createArticle(this.form)
    // 提示
    Message.success('添加成功')
    // 重新渲染 将当前页重置到第一页
    this.current = 1
    this.initData()
    // 关闭抽屉
    this.isShowDrawer = false
  } catch (e) {
    console.log(e)
  }
}

十四、添加功能-解决关闭重置表单功能

1.目标:

重置表单 → 解决关闭抽屉后,重新打开,内容还在的问题

image.png

2.代码实现

1.把关闭和重置功能 封装到单独的方法中

handleClose () {
  this.$refs.form.resetFields()
  this.isShowDrawer = false
}, 

2.添加成功后,调用关闭方法

async submit () {
  try {
    // 校验 (对整个表单校验)
    await this.$refs.form.validate()
    // 请求
    await createArticle(this.form)
    // 提示
    Message.success('添加成功')
    // 重新渲染 将当前页重置到第一页
    this.current = 1
    this.initData()
    // 关闭抽屉
    this.handleClose()
  } catch (e) {
    console.log(e)
  }
}

3.点击取消时,调用关闭方法

<el-button @click="handleClose">取消</el-button>

十五、面经管理 - 删除功能

1.目标

实现删除功能

2.思路

image.png

api/article.js

export const removeArticle = id => {
  return request.delete('/admin/interview/remove', {
    data: {
      id
    }
  })
}

页面中,注册点击事件调用

<i class="el-icon-delete" @click="del(row.id)"></i>

async del (id) {
  // 删除请求
  await removeArticle(id)
  // 添加成功的提示
  this.$message.success('删除成功')

  // 处理删除当前页的最后一条
  if (this.tableData.length === 1 && this.current > 1) {
     this.current--
  }
  // 重新渲染
  this.initData()
},

十六、面经管理 - 修改功能

1.目标

​ 修改回显

2.说明

修改比添加多一层回显,显示弹框时,需要发送请求获取数据

3.实现思路

1.封装接口,获取对应 id 的文章详情

2.显示抽屉的同时,将数据存入 form 回显

4.代码实现

3.1 封装接口,获取对应 id 的文章详情

api/article.js

export const getArticleDetail = id => {
  return request.get('/admin/interview/show', {
    params: {
      id
    }
  })
}

3.2 显示抽屉的同时,将数据存入 form 回显

async openDrawer (type, id) {
  // console.log(type, id)
  this.drawerType = type
  this.isShowDrawer = true

  if (type !== 'add') {
    const res = await getArticleDetail(id)
    this.form = {
      ...res.data
    }
  }
},

3.3关闭抽屉时,重置表单中的额外的数据

 closeDrawer () {
   // 将form也手动重置
    this.form = {
      stem: '', // 标题
      content: '' // 内容
    } 
    this.$refs.form.resetFields() // 重置表单 
    this.isShowDrawer = false // 关闭抽屉
 },

十七、修改提交

1.目标

完成修改提交功能

2.说明

修改 和 添加 共用同一个按钮,需要进行判断才能复用

3.实现思路

1.封装根据 id 进行提交修改的接口

2.按钮逻辑中添加判断,区分不同场景,调用接口完成功能

4.代码实现

4.1api/article.js准备api

export const updateArticle = data => {
  return request.put('/admin/interview/update', data)
}

4.2判断,修改提交

async submit () {
  try {
    // 校验表单
    await this.$refs.form.validate()
    // 如何区分, 当前是 编辑 还是 添加
    if (this.drawerType === 'add') {
      // 发送请求
      await createArticle(this.form)
      // 添加提示 $message.success
      this.$message.success('添加成功')
    }
    if (this.drawerType === 'edit') {
      // 发送的是编辑的请求
      const { id, stem, content } = this.form
      await updateArticle({ id, stem, content })
      this.$message.success('修改成功')
    }
    // 无论是修改还是添加, 都会回到第一页, 重置页码
    this.current = 1
    // 重新渲染
    this.initData()
    // 关闭弹框
    this.handleClose()
  } catch (e) {
    console.log(e)
  }
}

十八、面经管理 - 预览功能

1.目标

完成预览功能

2.说明

其实预览已经完成了,也是进行回显,只是不是利用表单回显,而是直接回显文章内容

预览不需要展示表单,直接 v-html 渲染即可

<div v-if="drawerType === 'preview'" class="article-preview">
  <h5>{{ form.stem }}</h5>
  <div v-html="form.content"></div>
</div>
<el-form v-else :model="form" :rules="rules" ref="form" label-width="80px">
  ...
</el-form>

处理关闭逻辑

handleClose () { 
  this.form = { stem: '', content: '' }
  // 注意点: 由于公用的抽屉, 当预览时, 是没有表单的! 不能重置表单 
  if (this.drawerType !== 'preview') {
    // add edit 调用 resetFields 在此处的作用: 重置校验状态
    this.$refs.form.resetFields()
  }
  this.isShowDrawer = false // 关闭弹框
},