问道后台管理项目
第一章:项目初始化
一、初始化目录结构
1. 创建工程
vue create wd-pc -m pnpm在选择样式预处理器的时候,选择 Sass/SCSS,其他不变。
2. 清理默认内容
1)删除默认组件、路由组件与静态资源。

2)重置 @/router/index.js 和 App.vue。
3. 路由设计
1)VSCode 设置用户片段
安装 Vue 3 Snippets 插件后,才会有 vue.json文件。不安装插件没有 vue.json。
需要在 VSCode 编辑器中配置代码片段:设置 ==> user snippets ==> vue.json。
使用 https://snippet-generator.app 生成。
<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.vue3)配置路由
router/index.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
pnpm add axios2)封装
创建 @/utils/request.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 request2. 引入 element-ui
1)安装
官方文档:Element 2.x 文档
pnpm add element-ui2)引入 Element
在 @/main.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 组件。
<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
/* 改变主题色变量 */
$--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
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 的组件:
<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 样式穿透
<style scoped>
父元素 >>> 子元素 {
color: red;
}
</style>less 样式穿透
<style lang="less" scoped>
父元素 /deep/ 子元素 {
color: red
}
</style>scss 样式穿透
<style scoped>
父元素::v-deep 子元素 {
color: red
}
</style>二、双向数据绑定
设置好数据,然后和输入框进行双向绑定。双向绑定的目的有两个:
- 点击登录的时候,可以获取到输入框中的数据
- 为下面的表单校验做准备
export default {
name: 'LoginPage',
data () {
return {
user: {
username: '',
password: ''
}
}
}
}页面中,使用 v-model="" 进行数据绑定:
<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中的一个属性

下面是对用户名、密码的基础验证代码:
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. 提交验证
上述验证有提示,但是如果用户强行点击登录呢?
下面给登录按钮绑定单击事件,输出一下输入框的数据,进行测试。
<el-button type="primary" @click="onSubmit">登 录</el-button>
methods: {
onSubmit () {
console.log(this.user)
}
}输入框故意输入很长的字符,然后点击登录。会发现一样可以得到输入框中的数据。
设想,如果此时直接提交数据到接口,那么表单验证就会形同虚设了。
所以,在点击登录按钮的时候,还需要进行完整验证:
methods: {
onSubmit () {
// 注意,给 <el-form> 加入ref属性,下面的代码自行修改
this.$refs['表单ref值'].validate((valid) => {
if (valid) {
// 通过验证
} else {
// 验证失败
}
})
}
}四、登录逻辑
1. 封装 API 方法
新建 @/api/user.js,封装登录的 API 方法:
import request from '@/utils/request'
export const loginAPI = (data) => {
return request.post('/auth/login', data)
}2. 组件调用 API 完成登录请求
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
一、结构搭建
<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 使用学习
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 | 菜单类型 | string | horizontal / vertical | vertical |
| collapse | 是否水平折叠收起菜单(仅在 mode 为 vertical 时可用) | boolean | — | false |
| router | 是否使用 vue-router 模式 | boolean | — | false |
| default-active | 当前激活菜单的 index | string | — | — |
| background-color | 菜单的背景色 | string | — | #ffffff |
| text-color | 菜单的文字颜色 | string | — | #303133 |
| active-text-color | 当前激活菜单的文字颜色 | string | — | #409EFF |
2. 项目中使用 NavMenu
<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 方法。
export const getUser = () => {
return request.get('/auth/currentUser')
}@/layout/index.vue 获取数据:
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 页面渲染:
<div class="user">
<el-avatar :size="36" :src="avatar"></el-avatar>
<el-link :underline="false">{{ name }}</el-link>
</div>三、退出功能
点击退出的时候,使用了 element-ui 的一个询问提示(还有其他询问提示哦~)
案例中使用的提示参考:Popconfirm 气泡确认框
首先给组件添加 @confirm 事件:
<div class="logout">
<el-popconfirm title="您确定要退出问道运营后台吗?" @confirm="logout">
<i #reference title="logout" class="el-icon-switch-button"></i>
</el-popconfirm>
</div>补充 logout 方法:
logout () {
localStorage.removeItem('mj-pc-token')
this.$router.push('/login')
}第四章:数据看板
一、页面布局
点我查看代码
<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)安装
pnpm add echarts2)导入
import * as echarts from 'echarts'3)添加 ref
<el-card style="height: 504px" shadow="never">
<div ref="chartBox" class="chart-box" style="height: 500px"></div>
</el-card>4)封装 API 方法,获取接口数据
// 获取图表用的数据
export function getLineData () {
return request({
url: '/analysis/DailyVisitTrend'
})
}5)组件中(@/views/dashboard/index.vue),导入 API 方法,在 mounted 中发送请求,渲染图标:
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
点我查看代码
<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>只给出了最基本的布局,重要的数据展示部分未给出。

<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。
// 获取面经列表
export const ArticleListAPI = (params) => {
return request({
url: '/admin/interview/query',
method: 'get',
params
})
}@/views/article/index.vue created 中发送初始化获取数据的请求。
import { ArticleListAPI } from '@/api/article'
// 重要代码
methods: {
async initData () {
const res = await ArticleListAPI()
console.log(res)
this.tableData = res.data.rows
}
},
created () {
this.initData()
}2. 渲染页面
<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. 分页渲染
<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>在项目中添加分页组件。
<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,然后发送新请求即可。
// 每页条数改变的回调
handleSizeChange (val) {
console.log('用户改变了每页显示的条数:' + val)
this.pageSize = val
this.current = 1
this.initData()
},
// 当前页改变的回调
handleCurrentChange (val) {
console.log('用户改变了当前页:' + val)
this.current = val
this.initData()
}三、 操作按钮部分
<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>添加删除方法:
// 删除方法
del (id) {
console.log(id)
}四、数据修改
1. 页面布局
1)使用抽屉
<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。
// 打开抽屉
openDrawer (type, id) {
console.log(type, id)
this.drawer = true
}3)计算属性控制标题
使用一个数据项 drawerType, 记录操作类型。然后根据操作类型,计算抽屉的标题。
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> 换成如下表单:
<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)富文本编辑器
wangEditor 的中文文档:https://www.wangeditor.com
TinyMCE(部分功能收费):https://www.tiny.cloud/docs/tinymce/latest
vue-quill-editor 的文档(项目中用这个):https://www.npmjs.com/package/vue-quill-editor
Quill 的中文文档:https://www.kancloud.cn/liuwave/quill/1409423
使用步骤:
1)装包
pnpm add vue-quill-editor2)导入(局部注册)
@/views/article/index.vue
// 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 绑定数据
<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)添加非空校验
<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>2. 添加功能
1)在 api/article.js 中封装接口 api
export const createArticleAPI = data => {
return request.post('/admin/interview/create', data)
}2)发送请求添加、关闭弹框 (关闭弹框要重置表单)、重新渲染列表
<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
// 删除方法
export const removeArticleAPI = (id) => {
// return request.delete('/admin/interview/remove', {
// data: { id }
// })
return request({
method: 'DELETE',
url: '/admin/interview/remove',
data: { id }
})
}页面中,注册点击事件调用。
可以添加询问框。询问框参考:MessageBox 弹框
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。
// 根据面经ID,查找该面经详细信息
export const ArticleDetailAPI = id => {
return request.get('/admin/interview/show', {
params: {
id
}
})
}回显展示:
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。
// 更新面经
export const updateArticleAPI = data => {
return request.put('/admin/interview/update', data)
}判断,修改提交。
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 渲染即可。
<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>处理关闭逻辑,有表单则重置。
handleClose () {
// ?. 是新语法,ES6语法
// 前面的表单如果能够找到,不是undefined,则调用后面的 resetFields 方法
this.$refs.form?.resetFields()
this.drawer = false
},第六章:将 token 存到 Vuex
一、存储 token 到 vuex
之前,是将 token 存储到 localStorage 中。
这个项目将 token 存储到 vuex 仓库中,原因是:
- token 在其他组件、模块中都会用得到,属于公共数据。
- vuex 的仓库在内存中,读取、更新速度非常快。
- 有完备的语法来操作 vuex 仓库中的数据。
实现步骤:
1)新建 @/store/modules/user.js
export default {
namespaced: true, // 开启命名空间
state: {
token: ''
},
mutations: {
updateToken (state, val) {
state.token = val
}
}
}2)@/store/index.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 仓库
this.$store.commit('user/updateToken', res.data.token)4)退出的时候,移除 token(修改 token 的值为空)
this.$store.commit('user/updateToken', '')5)请求拦截器,获取 token
// 先导入store
import store from '@/store'
// 请求拦截器中,带请求头这里
config.headers.Authorization = 'Bearer ' + store.state.user.token6)响应拦截器,如果是 401 错误,移除 token
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 中,导入,创建对象,并设置为插件。
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 对象之后,在导出之前添加导航守卫。
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')
}
}
})更简单的写法
// 加入导航守卫:如果没有登录,则不允许访问其他页面
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 中,加入两个配置:
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 如果打包结果,希望以File协议(双击打开),需要加如下的配置:
publicPath: '', // 这个值写成空字符串,或者 './' 都可以
// 下面的配置,作用是取消生成 xxx.map 文件
productionSourceMap: false
})三、生成打包报告
在 package.json 中,build 命令后加入 --report,这样的话,打包之后就会生成一个 report.html 文件,这个文件就记录着打包的结果的详细信息。
"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 等等。
<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 ..........
打包的时候,配置一下,哪些第三方插件不需要被打包。
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 完整的配置:
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 完整的配置:
<!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>配置之后,再打包,就会发现体积非常小了。
去哪里找第三方包: