Skip to content

问道后台管理项目

第一章:项目初始化

一、初始化目录结构

1. 创建工程

bash
vue create wd-pc -m pnpm

在选择样式预处理器的时候,选择 Sass/SCSS,其他不变。

2. 清理默认内容

1)删除默认组件、路由组件与静态资源。

2)重置 @/router/index.jsApp.vue

3. 路由设计

1)VSCode 设置用户片段

安装 Vue 3 Snippets 插件后,才会有 vue.json文件。不安装插件没有 vue.json。

需要在 VSCode 编辑器中配置代码片段:设置 ==> user snippets ==> vue.json。

使用 https://snippet-generator.app 生成。

vue
<template>
  <div class="${1:home}">${1:home}</div>
</template>

<script>
export default {
  name: '${2:HomeComponent}',
  data () {
    return {}
  },
  methods: {}
}
</script>

<style lang="${3:less}" scoped></style>
2)创建各个组件
| - views
	| - login             登录
  	  | - index.vue
	| - layout            首页架子
  	  | - index.vue
	| - article           文章列表
  	  | - index.vue
	| - dashboard         数据看板
  	  | - index.vue
3)配置路由

router/index.js

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

Vue.use(VueRouter)

const routes = [
  { path: '/login', component: () => import('@/views/login') },
  {
    path: '/',
    component: () => import('@/views/layout'),
    redirect: '/dashboard',
    children: [
      { path: 'dashboard', component: () => import('@/views/dashboard') },
      { path: 'article', component: () => import('@/views/article') }
    ]
  }
]

const router = new VueRouter({
  routes
})

export default router

马上在父组件里添加 <router-view></router-view>

二、导入资源文件

静态资源导入到项目的 @/assets 文件夹中。

三、引入项目依赖

1. 引入 axios

1)安装 axios

bash
pnpm add axios

2)封装

创建 @/utils/request.js,初始代码如下:

js
import router from '@/router'

// 导入 axios
import axios from 'axios'

// 配置
const request = axios.create({
  baseURL: 'http://interview-api-t.itheima.net'
})

// 添加请求拦截器
request.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么 --- 配置请求头
    const token = localStorage.getItem('mj-pc-token')
    config.headers.Authorization = `Bearer ${token}`
    return config
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)

// 添加响应拦截器
request.interceptors.response.use(
  function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    // 直接提取真实的响应,然后返回。后面组件中再使用的时候,就不用解构赋值了,直接可以得到真实的响应结果了
    return response.data
  },
  function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    // 下面判断,服务器如果有返回结果,则...............
    if (error.response && error.response.status === 401) {
      localStorage.removeItem('mj-pc-token')
      router.push('/login')
    }
    return Promise.reject(error)
  }
)

// 导出
export default request

2. 引入 element-ui

1)安装

官方文档:Element 2.x 文档

bash
pnpm add element-ui
2)引入 Element

@/main.js 中写入以下内容:

js
import Vue from 'vue';
import App from './App.vue';

// 完整引入 Element
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

new Vue({
  el: '#app',
  render: h => h(App)
});
3)检查是否引入成功

随便在 @/App.vue 文件中引入 Element 组件。

vue
<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>
4)自定义主题

文档:自定义主题

新建 @/styles/index.scss

scss
/* 改变主题色变量 */
$--color-primary: rgba(114,124,245,1);
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";

body {
  margin: 0;
  padding: 0;
  background: #fafbfe;
}

@/main.js 引入 index.scss

js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import '@/styles/index.scss'

Vue.use(ElementUI)
Vue.config.productionTip = false

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

第二章: 登录模块

一、页面布局

使用到 Element 的组件:

vue
<template>
  <div class="login-page">
    <el-card>
      <template #header>问道运营后台</template>
      <el-form autocomplete="off" label-width="60px">
        <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 class="tc">
          <el-button type="primary">登 录</el-button>
          <el-button>重 置</el-button>
        </el-form>
      </el-form>
    </el-card>
  </div>
</template>

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

<style scoped lang="scss">
// Element-UI 中,标签名,就是类名

.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 40px;
  }
  .tc {
    text-align: center;
  }
}
</style>

样式穿透

css 样式穿透

vue
<style scoped>
  父元素 >>> 子元素 {
     color: red;
  }
</style>

less 样式穿透

vue
<style lang="less" scoped>
  父元素 /deep/ 子元素 {
     color: red
  }
</style>

scss 样式穿透

vue
<style scoped>
  父元素::v-deep 子元素 {
     color: red
  }
</style>

二、双向数据绑定

设置好数据,然后和输入框进行双向绑定。双向绑定的目的有两个:

  1. 点击登录的时候,可以获取到输入框中的数据
  2. 为下面的表单校验做准备
js
export default {
  name: 'LoginPage',
  data () {
    return {
      user: {
        username: '',
        password: ''
      }
    }
  }
}

页面中,使用 v-model="" 进行数据绑定:

vue
<el-input v-model="user.username" ......其他属性略></el-input>

<el-input v-model="user.password" ......其他属性略></el-input>

三、表单验证

1. 页面验证

参考链接:Element Form 表单验证

必须的三个对应关系:

  • <el-form :model="数据项">
  • <el-form :rules="验证规则对象"> 验证规则对象和数据项平级,用于写验证规则
  • <el-form-item prop="字段"> === 数据项中的某个字段 === rules中的一个属性

下面是对用户名、密码的基础验证代码:

js
export default {
  name: 'LoginIndex',
  data () {
    return {
      user: {
        username: 'admin',
        password: 'admin'
      },
      rules: {
        username: [
          { required: true, message: '请输入用户名', trigger: ['blur', 'change'] },
          { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: ['blur', 'change'] }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: ['blur', 'change'] },
          { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: ['blur', 'change'] }
        ]
      }
    }
  }
}

2. 提交验证

上述验证有提示,但是如果用户强行点击登录呢?

下面给登录按钮绑定单击事件,输出一下输入框的数据,进行测试。

vue
<el-button type="primary" @click="onSubmit">登 录</el-button>
methods: {
  onSubmit () {
    console.log(this.user)
  }
}

输入框故意输入很长的字符,然后点击登录。会发现一样可以得到输入框中的数据。

设想,如果此时直接提交数据到接口,那么表单验证就会形同虚设了。

所以,在点击登录按钮的时候,还需要进行完整验证:

js
methods: {
  onSubmit () {
    // 注意,给 <el-form> 加入ref属性,下面的代码自行修改
    this.$refs['表单ref值'].validate((valid) => {
      if (valid) {
        // 通过验证
      } else {
        // 验证失败
      }
    })
  }
}

四、登录逻辑

1. 封装 API 方法

新建 @/api/user.js,封装登录的 API 方法:

js
import request from '@/utils/request'

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

2. 组件调用 API 完成登录请求

Message 消息提示

js
onSubmit () {
  this.$refs.form.validate(async valid => {
    if (valid) {
      try {
        const res = await loginAPI(this.user)
        localStorage.setItem('wd-pc-token', res.data.token)
        this.user = {
          username: '',
          password: ''
        }
        this.$message.success('登录成功')
        this.$router.push('/')
      } catch (error) {
        console.log(error)
        if (error.response) {
          this.$message.error(error.response.data.message)
        } else {
          this.$message.error('登录失败')
        }
      }
    }
  })

第三章:layout view

一、结构搭建

Container 布局容器

vue
<el-container>
  <el-aside width="200px">Aside</el-aside>
  <el-container>
    <el-header>Header</el-header>
    <el-main>
      <router-view></router-view>
    </el-main>
  </el-container>
</el-container>

二、导航菜单

1. NavMenu 使用学习

NavMenu 导航菜单

1)结构
  • <el-menu> - 菜单容器

  • <el-menu-item> - 菜单项

    如果一个菜单项没有子项,使用这个。

    vue
    <el-menu-item index="/article">面经管理</el-menu-item>
  • <el-submenu> - 子菜单容器

    vue
    <el-submenu index="1">
      <template #title>一级菜单标题</template>
      <!-- 子项【二级菜单】 -->
      <el-menu-item index="1-1">二级菜单标题</el-menu-item>
      <el-menu-item index="1-2">选项2</el-menu-item>
    </el-submenu>
2)Menu Attribute
属性说明类型可选值默认值
mode菜单类型stringhorizontal / verticalvertical
collapse是否水平折叠收起菜单(仅在 mode 为 vertical 时可用)booleanfalse
router是否使用 vue-router 模式booleanfalse
default-active当前激活菜单的 indexstring
background-color菜单的背景色string#ffffff
text-color菜单的文字颜色string#303133
active-text-color当前激活菜单的文字颜色string#409EFF

2. 项目中使用 NavMenu

vue
<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>

二、展示头像和用户名

@/api/user.js 准备 api 方法。

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

@/layout/index.vue 获取数据:

js
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
    }
  }
}

@/layout/index.vue 页面渲染:

vue
<div class="user">
  <el-avatar :size="36" :src="avatar"></el-avatar>
  <el-link :underline="false">{{ name }}</el-link>
</div>

三、退出功能

点击退出的时候,使用了 element-ui 的一个询问提示(还有其他询问提示哦~)

案例中使用的提示参考:Popconfirm 气泡确认框

首先给组件添加 @confirm 事件:

vue
<div class="logout">
  <el-popconfirm title="您确定要退出问道运营后台吗?" @confirm="logout">
    <i #reference title="logout" class="el-icon-switch-button"></i>
  </el-popconfirm>
</div>

补充 logout 方法:

js
logout () {
  localStorage.removeItem('mj-pc-token')
  this.$router.push('/login')
}

第四章:数据看板

一、页面布局

点我查看代码
vue
<template>
  <div class="dashboard-page">
    <el-breadcrumb separator-class="el-icon-arrow-right">
      <el-breadcrumb-item>面经后台</el-breadcrumb-item>
      <el-breadcrumb-item>数据看板</el-breadcrumb-item>
    </el-breadcrumb>
    <el-row :gutter="20">
      <el-col :span="6">
        <el-card style="height: 140px" shadow="never">
          <i class="el-icon-user"></i>
          <h5 class="tit">活跃用户</h5>
          <h2 class="num">802</h2>
          <p class="tag"><i>↑ 5.23%</i> 最近一个月</p>
        </el-card>
        <el-card style="height: 140px" shadow="never">
          <i class="el-icon-tickets"></i>
          <h5 class="tit">平均访问量</h5>
          <h2 class="num">1298</h2>
          <p class="tag"><i class="red">↓ 8.56%</i> 截止最近一周</p>
        </el-card>
        <el-card class="row" style="height: 180px" shadow="never">
          <h4>Enhance your Campaign for better outreach →</h4>
          <img src="@/assets/img.svg" alt />
        </el-card>
      </el-col>
      <el-col :span="18">
        <el-card style="height: 504px" shadow="never">
          <div class="chart-box" style="height: 500px"></div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card style="height: 420px" shadow="never">
          <h4>性别分布情况</h4>
          <img style="margin-top: 60px" src="@/assets/chart-03.png" alt />
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card style="height: 420px" shadow="never">
          <h4>浏览访问情况</h4>
          <img src="@/assets/chart-01.svg" alt />
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card style="height: 420px" shadow="never">
          <h4>设备系统访问情况</h4>
          <img style="margin-top: 20px" src="@/assets/chart-02.svg" alt />
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

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

<style lang="scss" scoped>
.dashboard-page {
  .el-breadcrumb {
    margin-top: 10px;
    margin-bottom: 25px;
  }
  .el-card {
    margin-bottom: 20px;
    position: relative;
    &.row {
      h4 {
        width: 40%;
        float: left;
        font-size: 18px;
        margin-left: 5%;
      }
      img {
        width: 40%;
        float: left;
        margin-left: 10%;
        margin-top: 30px;
      }
    }
    [class^='el-icon'] {
      font-size: 30px;
      color: #ccc;
      position: absolute;
      right: 25px;
      top: 30px;
      font-weight: bold;
    }
    .tit {
      color: #6c757d;
      font-size: 14px;
      margin: 6px 0;
    }
    .num {
      color: #6c757d;
      font-size: 30px;
      margin: 6px 0;
    }
    .tag {
      color: #999;
      margin: 0;
      font-size: 14px;
      > i {
        font-style: normal;
        margin-right: 10px;
        color: rgb(10, 207, 151);
        &.red {
          color: #fa5c7c;
        }
      }
    }
    img {
      width: 100%;
      height: 100%;
    }
    h4 {
      margin: 0;
      padding-bottom: 20px;
      color: #6c757d;
    }
  }
}
</style>

二、使用 echarts

1)安装

bash
pnpm add echarts

2)导入

js
import * as echarts from 'echarts'

3)添加 ref

vue
<el-card style="height: 504px" shadow="never">
  <div ref="chartBox" class="chart-box" style="height: 500px"></div>
</el-card>

4)封装 API 方法,获取接口数据

js
// 获取图表用的数据
export function getLineData () {
  return request({
    url: '/analysis/DailyVisitTrend'
  })
}

5)组件中(@/views/dashboard/index.vue),导入 API 方法,在 mounted 中发送请求,渲染图标:

js
async mounted () {
  const res = await getLineData()
  this.myChart = echarts.init(this.$refs.chartBox)
  const option = {
    xAxis: {
      type: 'category',
      boundaryGap: false,
      // data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      data: res.data.list.map(item => item.ref_date)
    },
    yAxis: {
      type: 'value'
    },
    series: [
      {
        // data: [820, 932, 901, 934, 1290, 1330, 1320],
        data: res.data.list.map(item => item.visit_uv),
        type: 'line',
        lineStyle: {
          color: '#1bd4ae'
        },
        itemStyle: {
          color: '#1bd4ae'
        },
        smooth: true, // 平滑曲线
        // 面经区域样式
        areaStyle: {
          color: '#5ee0c6'
        }
      },
      {
        // data: [360, 230, 101, 234, 190, 130, 320],
        data: res.data.list.map(item => item.visit_uv_new),
        type: 'line',
        lineStyle: {
          color: '#5ab1ef'
        },
        itemStyle: {
          color: '#5ab1ef'
        },
        smooth: true, // 平滑曲线
        // 面经区域样式
        areaStyle: {
          color: '#5bbfe3'
        }
      }
    ]
  }
  this.myChart.setOption(option)
}

第五章:面经列表

一、基本页面布局

@/views/artcile/index.vue

点我查看代码
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 {}
  }
}
</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>

只给出了最基本的布局,重要的数据展示部分未给出。

Element Table 表格

vue
<el-table :data="tableData" 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>

tableData 中放表单数据。数据格式是数组里面放一个个对象,这一个个对象就是每一行的内容。

  • :data="tableData":表单数据
  • prop="date":设置数据源中对象中的键名,即可填入数据
  • label:列名
  • width:列宽

二、数据展示

1. 发送网络请求

新建 @/api/article.js

js
// 获取面经列表
export const ArticleListAPI = (params) => {
  return request({
    url: '/admin/interview/query',
    method: 'get',
    params
  })
}

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

js
import { ArticleListAPI } from '@/api/article'

// 重要代码
methods: {
  async initData () {
    const res = await ArticleListAPI()
    console.log(res)
    this.tableData = res.data.rows
  }
},
created () {
  this.initData()
}

2. 渲染页面

vue
<el-table :data="tableData" 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>

3. 分页渲染

Element Pagination 分页

vue
<el-pagination
  @size-change="handleSizeChange" <!-- 每页条数改变的回调 -->
  @current-change="handleCurrentChange" <!-- 当前页改变的回调 -->
  :current-page="currentPage" <!-- 当前页码,支持 .sync 修饰符 -->
  :page-sizes="[100, 200, 300, 400]" <!-- 每页显示个数选择器的选项设置 -->
  :page-size="100" <!-- 每页显示条目个数,支持 .sync 修饰符 -->
  layout="total, sizes, prev, pager, next, jumper" <!-- 组件布局,子组件名用逗号分隔 -->
  :total="400"> <!-- 总条目数 -->
</el-pagination>

在项目中添加分页组件。

vue
<el-pagination
  @size-change="handleSizeChange"
  @current-change="handleCurrentChange"
  :current-page="current"
  :page-sizes="[10, 15, 20, 30]"
  :page-size="pageSize"
  layout="total, sizes, prev, pager, next, jumper"
  :total="total">
</el-pagination>

有了提供的分页组件,实现分页简单了。在 data 选项定义每页显示的条数与当前页。

当用户点击组件的上一页与下一页就会自动触发 currentPage 函数,在此函数修改 data 选项的当前页,然后发送新请求即可。

当用户点击组件的修改每页显示条数就会自动触发 handleSizeChange,只需在此函数修改 data 选项的 pageSize,然后发送新请求即可。

js
// 每页条数改变的回调
handleSizeChange (val) {
  console.log('用户改变了每页显示的条数:' + val)
  this.pageSize = val
  this.current = 1
  this.initData()
},
// 当前页改变的回调
handleCurrentChange (val) {
  console.log('用户改变了当前页:' + val)
  this.current = val
  this.initData()
}

三、 操作按钮部分

vue
<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>

添加删除方法:

js
// 删除方法
del (id) {
  console.log(id)
}

四、数据修改

1. 页面布局

1)使用抽屉

Element Drawer 抽屉

Vue
<el-drawer
  title="我是标题" <!-- 抽屉标题 -->
  :visible.sync="drawer" <!-- 是否显示抽屉 -->
  :with-header="true" <!-- false 关闭抽屉的标题 -->
  :before-close="handleClose" <!-- 关闭前的回调,会暂停 Drawer 的关闭 -->
  close-on-press-escape="true"> <!-- 是否可以通过按下 ESC 关闭 Drawer -->
  <span>我来啦!</span> <!-- 内容 -->
</el-drawer>
2)复用抽屉

目标:使用一个抽屉,并且还能区分是什么操作。

所以创建打开抽屉的方法 openDrawer,并且在添加、编辑、预览的时候,调用该方法,并且传递一个参数,用于表示是什么操作。

  • 添加。@click="openDrawer('add')"
  • 预览。@click="openDrawer('view', row.id)"
  • 编辑。@click="openDrawer('edit', row.id)"

预览和编辑,还需要传递 id。

js
// 打开抽屉
openDrawer (type, id) {
  console.log(type, id)
  this.drawer = true
}
3)计算属性控制标题

使用一个数据项 drawerType, 记录操作类型。然后根据操作类型,计算抽屉的标题。

js
data () {
  return {
    drawer: false,
    drawerType: '' // add-添加、edit-修改、view-预览等类型
  }
},
methods: {  
    openDrawer (type, id) {
      this.drawerType = type
      this.drawer = true
    },
},

computed: {
  drawerTitle () {
    let title = '大标题'
    if (this.drawerType === 'add') title = '添加面经'
    if (this.drawerType === 'view') title = '面经预览'
    if (this.drawerType === 'edit') title = '修改面经'
    return title
  }
},
4)表单结构

<span>我来了</span> 换成如下表单:

Vue
<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">
    富文本编辑器
  </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: ''
    }
  }
}
5)富文本编辑器

使用步骤:

1)装包

bash
pnpm add vue-quill-editor

2)导入(局部注册)

@/views/article/index.vue

js
// 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
  }
}

3)使用。 v-model 绑定数据

vue
<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>
6)添加非空校验
vue
<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' }]
}

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

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

2. 添加功能

1)在 api/article.js 中封装接口 api

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

2)发送请求添加、关闭弹框 (关闭弹框要重置表单)、重新渲染列表

js
<el-button type="primary" @click="onSubmit">确认</el-button>



import { ArticleListAPI, createArticleAPI } from '@/api/article'

// 关闭的方法
handleClose () {
  this.$refs.form.resetFields() // 重置表单
  this.drawer = false
},


// 提交表单数据的方法
onSubmit () {
  this.$refs.form.validate(async (valid) => {
    if (valid) {
      await createArticleAPI(this.form)
      this.$message.success('添加成功')
      this.current = 1
      this.initData()
      this.handleClose()
    }
  })
}

3. 删除功能

1)在 api/article.js 中封装接口 api

js
// 删除方法
export const removeArticleAPI = (id) => {
  // return request.delete('/admin/interview/remove', {
  //   data: { id }
  // })
  return request({
    method: 'DELETE',
    url: '/admin/interview/remove',
    data: { id }
  })
}

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

可以添加询问框。询问框参考:MessageBox 弹框

js
import { ArticleListAPI, createArticleAPI, removeArticleAPI } from '@/api/article'

// 删除方法
del (id) {
  this.$confirm('你确定要删除吗?', '提示')
    .then(async () => {
      // 说明点击了确认
      // console.log(id)
      await removeArticleAPI(id)
      this.$message.success('删除成功')
      this.initData()
    })
    .catch(() => {
      // 说明点击了取消
    })
},

4. 修改功能

1)回显展示

@api/article.js 中添加 API。

js
// 根据面经ID,查找该面经详细信息
export const ArticleDetailAPI = id => {
  return request.get('/admin/interview/show', {
    params: {
      id
    }
  })
}

回显展示:

js
import { 
  ArticleListAPI, 
  createArticleAPI, 
  removeArticleAPI,
  ArticleDetailAPI
} from '@/api/article'



async openDrawer (type, id) {
  this.drawerType = type
  this.drawer = true

  if (type !== 'add') {
    const res = await ArticleDetailAPI(id)
    this.form.stem = res.data.stem
    this.form.content = res.data.content
    this.form.id = res.data.id // 加入 id,一会更新用
  }
},
2)修改提交

api/article.js 准备 api。

js
// 更新面经
export const updateArticleAPI = data => {
  return request.put('/admin/interview/update', data)
}

判断,修改提交。

js
import { 
  ArticleListAPI, 
  createArticleAPI, 
  removeArticleAPI,
  ArticleDetailAPI,
  updateArticleAPI
} from '@/api/article'



// 点击表单中的确认,触发的方法【可能是添加操作、可能是更新操作】
onSubmit () {
  // 无论添加、还是修改,都要进行完整的校验
  this.$refs.form.validate(async valid => {
    if (valid) {
      // 验证通过,此时可以发送请求
      if (this.drawerType === 'add') {
        // 添加面试题
        await addArticleAPI(this.form)
        this.$message.success('添加成功') // 提示
      } else {
        // 修改面试题
        await udpateArticleAPI(this.form)
        this.$message.success('修改成功') // 提示
        // 重置form数据,为 最开始的样子
        this.form = {
          stem: '',
          content: ''
        }
      }
      this.current = 1
      this.initData() // 更新页面数据
      this.handleClose() // 关闭抽屉
    }
  })
}

5. 预览功能

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

vue
<div v-if="drawerType === 'view'" 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>

处理关闭逻辑,有表单则重置。

js
handleClose () {
  // ?. 是新语法,ES6语法
  // 前面的表单如果能够找到,不是undefined,则调用后面的 resetFields 方法
  this.$refs.form?.resetFields()
  this.drawer = false
},

第六章:将 token 存到 Vuex

一、存储 token 到 vuex

之前,是将 token 存储到 localStorage 中。

这个项目将 token 存储到 vuex 仓库中,原因是:

  1. token 在其他组件、模块中都会用得到,属于公共数据。
  2. vuex 的仓库在内存中,读取、更新速度非常快。
  3. 有完备的语法来操作 vuex 仓库中的数据。

实现步骤:

1)新建 @/store/modules/user.js

js
export default {
  namespaced: true, // 开启命名空间
  state: {
    token: ''
  },
  mutations: {
    updateToken (state, val) {
      state.token = val
    }
  }
}

2)@/store/index.js 中导入模块,并注册模块。

js
import Vue from 'vue'
import Vuex from 'vuex'
// 引入user模块
import user from '@/store/modules/user'
Vue.use(Vuex)

export default new Vuex.Store({
  strict: true, // 此项可加可不加
  modules: { user }
})

3)登录成功后,将 token 存储到 vuex 仓库

js
this.$store.commit('user/updateToken', res.data.token)

4)退出的时候,移除 token(修改 token 的值为空)

js
this.$store.commit('user/updateToken', '')

5)请求拦截器,获取 token

js
// 先导入store
import store from '@/store'

// 请求拦截器中,带请求头这里
config.headers.Authorization = 'Bearer ' + store.state.user.token

6)响应拦截器,如果是 401 错误,移除 token

js
store.commit('user/updateToken', '')

7)检查,登录后,尝试使用 vue 调试工具,查看 vuex 中是否有 token。

二、持久化存储 token

上述存储 token,也有一定的问题,当页面刷新后,vuex 中的数据就会重置(毕竟是存在内存中),所以我们还需要将 vuex仓库中的数据,同步到 localStorage 中。这样数据就可以持久化存储了。

这里的实现方案有两个,一是自己写代码。二是使用插件。

我们选择使用插件:vuex-persist ,类似的插件还有 vuex-along 等等,非常多。

具体使用步骤:

1)安装模块 pnpm add vuex-persist

2)@/store/index.js 中,导入,创建对象,并设置为插件。

js
import Vue from 'vue'
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist'
import user from '@/store/modules/user'
Vue.use(Vuex)

const vuexLocal = new VuexPersistence({
  storage: window.localStorage
})

export default new Vuex.Store({
  // strict: true, // 此项可加可不加
  modules: { user },
  plugins: [vuexLocal.plugin]
})

设置完毕。

  • 登录成功后

    会将 vuex 仓库的数据存储到 localStorage 中

  • 刷新页面后

    会将 localStorage 中的数据恢复到 vuex仓库中

第七章:加入导航守卫

@/router/index.js 中,创建 router 对象之后,在导出之前添加导航守卫。

js
import store from '@/store'

// 加入全局前置导航守卫
router.beforeEach((to, from, next) => {
  // to.path ---- 即将要访问的hash地址
  // from.path ---- 从哪里来的,上一个hash地址是什么
  // next()--放行  next(false)--不放行  next('/login')--跳转到指定的地址
  if (to.path === '/login') {
    next() // 访问的是登录,无条件的放行
  } else {
    // 说明访问的不是登录页。 还得判断是否登录过,如果登录过则允许访问,没有登录过不允许访问
    if (store.state.user.token) {
      next()
    } else {
      next('/login')
    }
  }
})

更简单的写法

js
// 加入导航守卫:如果没有登录,则不允许访问其他页面
router.beforeEach((to, from, next) => {
  // to.path ---- 要访问的地址
  // from.path ---- 你从哪里来的
  // next() ---- 放行       next('/login') ---- 不放行,并跳转到登录
  if (to.path !== '/login' && !store.state.user.token) {
    next('/login')
    return
  }
  next()
})

第八章:最终的打包

一、直接打包

执行 pnpm run build 可以直接打包,会将结果打包进 dist 文件夹。

二、配置

vue.config.js 中,加入两个配置:

js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  // 如果打包结果,希望以File协议(双击打开),需要加如下的配置:
  publicPath: '', // 这个值写成空字符串,或者 './' 都可以

  // 下面的配置,作用是取消生成 xxx.map 文件
  productionSourceMap: false
})

三、生成打包报告

package.json 中,build 命令后加入 --report,这样的话,打包之后就会生成一个 report.html 文件,这个文件就记录着打包的结果的详细信息。

json
"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build --report",
  "lint": "vue-cli-service lint"
},

打包之后,打开 dist 文件夹里面的 report.html,看一下哪个区域的面积最大,说明这个文件的体积就最大。

四、优化打包的体积

加载 CDN 链接,使用其他网站的 axios、echarts、element-ui 等等。

html
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.3.3/dist/echarts.min.js"></script>

加载之后,window 对象中,就多了对应的 axios、echarts ..........

打包的时候,配置一下,哪些第三方插件不需要被打包。

js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  // 如果打包结果,希望以File协议(双击打开),需要加如下的配置:
  publicPath: '', // 这个值 写错 空字符串,或者 './' 都可以

  // 下面的配置,作用是取消生成 xxx.map 文件
  productionSourceMap: false,

  // 配置排除哪些第三方包
  configureWebpack: {
    // 排除项
    externals: {
      // 键(第三方包名): 值(window对象中多了什么)
      echarts: 'echarts',
      axios: 'axios',
    }
  }
})

这样,打包之后,就不会把 axios、echarts 打包进我们的项目了,这样我们项目的体积就变得非常小了。

vue.config.js 完整的配置:

js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: './',
  // 下面的配置,作用是取消生成 xxx.map 文件
  productionSourceMap: false,

  // 配置排除哪些第三方包
  configureWebpack: {
    // 排除项
    externals: {
      // 键(第三方包名): 值(window对象中多了什么)
      axios: 'axios',
      vue: 'Vue',
      'vue-router': 'VueRouter',
      vuex: 'Vuex',
      echarts: 'echarts',
      'element-ui': 'ELEMENT',
      'vue-quill-editor': 'VueQuillEditor'
    }
  }
})

@/public/index.html 完整的配置:

html
<!DOCTYPE html>
<html lang="">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>
    <%= htmlWebpackPlugin.options.title %>
  </title>
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.9/lib/theme-chalk/index.css">
  <link rel="stylesheet" href="https://unpkg.com/quill@1.3.7/dist/quill.core.css">
  <link rel="stylesheet" href="https://unpkg.com/quill@1.3.7/dist/quill.snow.css">
  <link rel="stylesheet" href="https://unpkg.com/quill@1.3.7/dist/quill.bubble.css">
</head>

<body>
  <noscript>
    <strong>
      We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
      Please enable it to continue.
    </strong>
  </noscript>
  <div id="app"></div>
  <!-- built files will be auto injected -->
  <script src="https://unpkg.com/vue@2.6/dist/vue.min.js"></script>
  <script src="https://unpkg.com/vue-router@3.5.4/dist/vue-router.js"></script>
  <script src="https://unpkg.com/axios@0.27.2/dist/axios.min.js"></script>
  <script src="https://unpkg.com/vuex@3.6.2/dist/vuex.js"></script>
  <script src="https://unpkg.com/element-ui@2.15.9/lib/index.js"></script>
  <script src="https://unpkg.com/echarts@5.5.0/dist/echarts.min.js"></script>
  <script src="https://unpkg.com/quill@1.3.7/dist/quill.js"></script>
  <script src="https://unpkg.com/vue-quill-editor@3.0.6/dist/vue-quill-editor.js"></script>
</body>

</html>

配置之后,再打包,就会发现体积非常小了。

去哪里找第三方包:

Released under the MIT License.