使用命令创建一个项目。
vue create 项目名
比如
vue create vue-cms
如果前一天,已经存了预设,直接使用那个预设。
没有存预设,按照下面的步骤安装:
创建远程仓库
(本地已经有一次提交了)
添加远程仓库地址
git remote add origin 仓库地址
首次推送
git push -u origin master
找到项目中的 .eslintrc.js
,在 rules
中加入如下的配置项
// 修正函数名和小括号之间的空格问题
// 参考文档:https://eslint.bootcss.com/docs/rules/space-before-function-paren
'space-before-function-paren': [
'error',
{
anonymous: 'always', // 匿名函数小括号前,需要空格。比如 setTimeout(function () {})
named: 'never', // 有名字的方法,不需要空格。比如 abc() {}
asyncArrow: 'always' // 箭头函数,需要空格
}
]
App.vue,只留下基础的模板即可
<template>
<div id="app">大事件项目</div>
</template>
删除views里面的两个组件(Home.vue 和 About.vue )
删除 components 里面的 HelloWorld.vue 组件
重置路由(router/index.js)
import Vue from 'vue'
import VueRouter from 'vue-router'
// 去掉加载 Home 这行
Vue.use(VueRouter)
const routes = [] // 清空这个数组
const router = new VueRouter({
routes
})
export default router
项目的目录结构:
node_modules --- 我们下载的第三方包(不能动)
public --- 存放项目的首页(不能动)
src --- 我们编码的位置
- assets --- 存储项目的资源(图片等等)
- components --- 存储组件的(不受路由控制,一般放一下通用的、封装好的组件)
- views --- 存储组件的(受路由控制的组件,放到这里)
- router --- 配置文件的文件
- store --- vuex的配置
- App.vue --- 项目的根组件(其他所有组件都是它的孩子)
- main.js --- 它是打包的入口文件(main.js是最先执行的文件)
.eslintrc.js --- ESLint的配置文件
.gitignore --- git的忽略文件(已经写好,不需要处理了)
babel.config.js --- babel是对JS进行降价处理的(目前的项目不需要处理的)
package-lock.json --- 锁定第三方包的版本的文件(不需要动)
package.json --- 项目的配置文件
将 发的资料里面的 asset 文件夹里面的全部内容,复制到项目的 assets 文件夹中。
组件应该放到哪里?
在views里面创建 Login.vue (登录组件) 和 Reg.vue (注册组件)
配置路由(router/index.js)
import Login from '@/views/Login.vue'
Vue.use(VueRouter) // 这行是原有的
const routes = [
// 直接写到这个最大的数组中的路由,对应的组件,应该显示在App.vue的占位符位置
// 比如下面的 登录组件,应该显示在App.vue的占位符位置
// { path: '路由地址', component: '组件' }
{ path: '/login', component: Login }, // 使用组件应该先导入
{ path: '/reg', component: () => import('@/views/Reg.vue') }
]
App.vue 设置 <router-view></router-view>
占位符
<template>
<router-view></router-view>
</template>
一定要去掉原有的
<div id="app"></div>
测试
参考:https://element.eleme.cn/#/zh-CN/component/quickstart
安装:npm i element-ui
在main.js 中加入代码:
import Vue from 'vue';
+ import ElementUI from 'element-ui';
+ import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
+ Vue.use(ElementUI);
new Vue({
el: '#app',
render: h => h(App)
});
测试:
复制一个按钮到 Login.vue 中
<template>
<div>
<el-button type="primary">主要按钮</el-button>
</div>
</template>
打开页面(http://localhost:8080/#/login),看一下是否能够正常显示按钮。
public/index.html ,加入样式,设置html,body高100%
<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
</style>
Login.vue 里面,设置最大的盒子的样式
<template>
<div class="login-container"></div>
</template>
<style lang="less">
.login-container {
height: 100%;
background: url(../assets/images/login_bg.jpg) no-repeat;
background-size: cover;
}
</style>
将element-ui里面的卡片,复制一个,放到 <div class="login-container"></div>
里面
<template>
<div class="login-container">
<!-- 下面使用element的卡片,做中间的那个盒子 -->
<el-card shadow="hover">
这是一个卡片
</el-card>
</div>
</template>
设置卡片的样式(居中、高、宽)
.el-card {
height: 355px; // 高度随便加一个,最后完成后,去掉,让高度自动撑开
width: 400px;
position: absolute;
left: 50%; // 针对父级元素的50%
top: 50%;
transform: translate(-50%, -50%); // 向回平移自身的50%
}
<el-card>
里面加入登录标题 <div class="login-title"></div>
<template>
<div class="login-container">
<!-- 下面使用element的卡片,做中间的那个盒子 -->
<el-card shadow="hover">
<!-- 登录标题 -->
<div class="login-title"></div>
</el-card>
</div>
</template>
设置标题样式
.login-title {
height: 60px;
background: url(../assets/images/login_title.png) 50% no-repeat;
}
element-ui的表单参考:https://element.eleme.cn/#/zh-CN/component/form
复制 <el-form></el-form>
到 标题div后面
复制 三个表单项 (<el-form-item>......</el-form-item>
) 到 <el-form></el-form>
中
<!-- 登录标题 -->
<div class="login-title"></div>
<!-- 表单盒子 -->
<el-form ref="form">
<!-- 用户名 -->
<el-form-item>
<el-input></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
<el-input></el-input>
</el-form-item>
<!-- 按钮 -->
<el-form-item>
<el-input></el-input>
</el-form-item>
</el-form>
复制之后,去掉和数据相关的部分,否则报错
- 去掉
<el-form>
的:model
属性- 去掉
<el-input>
的v-model
属性去掉 label 和 label-width(输入框前面的文字去掉)
将最后一个输入框换成按钮,设置宽度 100%
<!-- 按钮 -->
<el-form-item>
<el-button type="primary">登录</el-button>
</el-form-item>
表单后面加入超链接,参考:https://element.eleme.cn/#/zh-CN/component/link
<!-- 表单后面,放一个超链接 -->
<el-link :underline="false">还没有账号,去注册</el-link>
将 .el-card 的高度去掉,让内容自动撑开
可以通过 prefix-icon
和 suffix-icon
属性在 input 组件首部和尾部增加显示图标
<!-- 用户名 -->
<el-form-item>
<el-input placeholder="请输入用户名" prefix-icon="el-icon-user"></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
<el-input placeholder="请输入密码" prefix-icon="el-icon-lock"></el-input>
</el-form-item>
参考文档:https://element.eleme.cn/#/zh-CN/component/input
type="password" show-password
<!-- 密码 -->
<el-form-item>
<el-input type="password" show-password placeholder="请输入密码" prefix-icon="el-icon-lock"></el-input>
</el-form-item>
自愿修改类名等等,把login改为reg
测试
现在,是手动修改地址栏,显示登录注册。
下面通过 编程式导航(@click="$router.push('hash地址')"
),实现两个页面之间的跳转。
Reg.vue
<!-- 表单后面,放一个超链接 -->
<el-link :underline="false" @click="$router.push('/login')">已有账号,去登录</el-link>
<!-- <el-link :underline="false" href="/#/login">已有账号,去登录</el-link> -->
<!-- <router-link to="/login">已有账号,去登录</router-link> -->
Login.vue
<!-- 表单后面,放一个超链接 -->
<el-link :underline="false" @click="$router.push('/reg')">还没有账号,去注册</el-link>
<!-- <el-link :underline="false" href="/#/reg">还没有账号,去注册</el-link> -->
<!-- <router-link to="/reg">还没有账号,去注册</router-link> -->
Reg.vue 的 JS中,设置一份数据
export default {
data() {
return {
// 1. 做双向数据绑定(data中放数据,v-model和input建立双向绑定关系)
userInfo: {
username: '',
password: '',
repassword: ''
}
}
}
// 2. 表单验证(减少一下不必要的请求)
// 3. 表单提交,完成注册功能
}
通过v-model 和三个输入进行双向绑定
<!-- 用户名 -->
<el-form-item>
<el-input
v-model="userInfo.username" <!-- 这里加入v-model -->
placeholder="请输入用户名"
prefix-icon="el-icon-user"
></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
<el-input
v-model="userInfo.password" <!-- 这里加入v-model -->
type="password"
show-password
placeholder="请输入密码"
prefix-icon="el-icon-lock"
></el-input>
</el-form-item>
<!-- 确认密码 -->
<el-form-item>
<el-input
v-model="userInfo.repassword" <!-- 这里加入v-model -->
type="password"
show-password
placeholder="请再次输入密码"
prefix-icon="el-icon-lock"
></el-input>
</el-form-item>
参考文档:https://element.eleme.cn/#/zh-CN/component/form
三个对应关系。
<el-form>
中的 :model
和数据项对应; :rules
和验证规则对应
<!-- :model和数据项对应 -->
<!-- :rules和验证规则对应 -->
<el-form
ref="form"
:model="userInfo"
:rules="rules"
>
<el-form-item>
中的 prop
和验证规则中的数据项对应
<el-form-item prop="username">
<el-form-item prop="password">
<el-form-item prop="repassword">
JS中,定义数据项(已经定义了userInfo)
JS中,定义验证规则(rules)
data() {
return {
// 1. 做双向数据绑定(data中放数据,v-model和input建立双向绑定关系)
userInfo: {
username: '',
password: '',
repassword: ''
},
// 2. 表单验证(减少一下不必要的请求)
rules: {
username: [
// required表示必填,message验证不通过时的提示,trigger,表示什么时候触发验证
{ required: true, message: '请输入用户名', trigger: 'blur' },
// { min: 1, max: 10, message: '用户名长度1~10位', trigger: 'blur' }
{
pattern: /^[a-zA-Z0-9]{1,10}$/,
message: '用户名必须是长度1-10位的字母数字',
trigger: 'blur'
}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 15, message: '密码长度6~15位', trigger: 'blur' }
],
repassword: [
{ required: true, message: '请输入确认密码', trigger: 'blur' },
{ min: 6, max: 15, message: '确认密码长度6~15位', trigger: 'blur' }
]
}
}
}
图示对应关系:
参考文档:https://element.eleme.cn/#/zh-CN/component/form
在 data
函数中,return
之前,自定义函数验证确认密码。
var validatePass2 = (rule, value, callback) => {
// rule 没什么用。
// value 表示使用这个规则的输入框。案例中,确认密码使用这个验证规则,所以value表示我们输入的确认密码值
// callback 表示是否通过验证
// 和密码进行比较
if (value !== this.userInfo.password) { // 这里的this.userInfo.password表示我们的密码
callback(new Error('两次输入密码不一致!')) // 验证不通过,传入错误对象
} else {
callback() // 验证通过,直接callback()
}
}
在确认密码验证时,加入 validator
,值就是上面的自定义函数
repassword: [
{ required: true, message: '请输入确认密码', trigger: 'blur' },
{ min: 6, max: 15, message: '确认密码长度6~15位', trigger: 'blur' },
{ validator: validatePass2, trigger: 'blur' }
]
找到提交按钮,注册单击事件
<el-button type="primary" @click="reg">注册</el-button>
补充 methods 方法(methods和data平级),处理单击事件
// 3. 表单提交,先进行完整的表单验证,验证通过再完成注册功能
methods: {
reg() {
// this.$refs.form 找到表单
// 调用 validate方法,进行表单完整的验证
this.$refs.form.validate(valid => {
// console.log(valid) // 布尔值,验证通过是true;验证不通过是false
if (!valid) return
console.log('验证通过,可以提交数据了')
})
}
}
request.js
reg.js
等
下载安装 axios
封装并导出 axios 的实例
在src中,创建 utils 的文件夹,里面创建 request.js
import axios from 'axios'
// 创建axios的实例。 这个实例中,可以对axios进行全局的配置
// 得到的 request 也是axios。只不过名字不能再叫axios,因为重名了
const request = axios.create({
baseURL: 'http://www.liulongbin.top:3008'
})
// const request2 = axios.create({
// baseURL: 'http://www.itcbc.com'
// })
export default request
封装注册请求的方法
在src中,创建 api 的文件夹,里面创建 reg.js
// 这里导入 axios的实例对象
import request from '@/utils/request'
// 按需导出封装后的方法
export const regAPI = data => {
return request.post('/api/reg', data)
}
Reg.vue 组件中,使用
按需导入 regAPI
方法
// 按需导入 API 方法
import { regAPI } from '@/api/reg.js'
当注册验证通过后,调用 regAPI
方法,完成注册功能
regAPI
方法返回的是 Promise
对象await
关键字await
最近的函数,加上 async
// 3. 表单提交,先进行完整的表单验证,验证通过再完成注册功能
methods: {
reg() {
// this.$refs.form 找到表单
// 调用 validate方法,进行表单完整的验证
// 下面的 form 是 <el-form> 的 ref 属性。作用是找到表单
this.$refs.form.validate(async valid => {
// console.log(valid) // 布尔值,验证通过是true;验证不通过是false
if (!valid) return
// console.log('验证通过,可以提交数据了')
const { data: res } = await regAPI(this.userInfo)
// console.log(res) // { code: 0, message: '提示消息' }
if (res.code === 0) {
alert(res.message)
} else {
alert(res.message)
}
})
}
}
if (res.code === 0) {
// alert(res.message)
// this.$message({ type: 'success', message: res.message })
this.$message.success(res.message)
// 清空输入框(重置表单) // form 是表单的 ref="form" 值
this.$refs.form.resetFields()
// 跳转到登录页
this.$router.push('/login')
} else {
this.$message({ type: 'error', message: res.message })
}
因为注册和登录绝大多数代码相同。
所以可以复制注册页的全部代码,到登录页。
修改不同的地方即可:
设置data中的 userInfo 数据
export default {
data() {
return {
// 1. 设置数据,和表单input进行双向绑定
userInfo: {
username: '',
password: ''
}
}
}
// 2. 表单验证
// 3. 通过验证,表单提交,完成登录
}
使用 v-model="xxxx" 进行双向数据绑定
<!-- 用户名 -->
<el-form-item>
<el-input
v-model="userInfo.username"
placeholder="请输入用户名"
prefix-icon="el-icon-user"
></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
<el-input
type="password"
v-model="userInfo.password"
show-password
placeholder="请输入密码"
prefix-icon="el-icon-lock"
></el-input>
</el-form-item>
userInfo
和 <el-form>
的 :model="userInfo"
对应rules
和 <el-form>
的 :rules="rules"
对应<el-form-item>
的 prop="xxx"
对应具体代码,见 Login.vue
参考文档:https://element.eleme.cn/#/zh-CN/component/form 里面的表单验证
给 按钮,添加单击事件
<el-button type="primary" @click="login">登录</el-button>
methods中 login方法,进行完整的验证
// 3. 通过验证,表单提交,完成登录
methods: {
login() {
this.$refs.form.validate(valid => {
if (!valid) return // 表单验证未通过,直接 return
// 表单验证通过,发送请求,完成登录
console.log('通过验证')
})
}
}
reg.js
中封装并导出 loginAPI
// 按需导出 登录方法
// export const loginAPI = data => {
// return request.post('/api/login', data)
// }
export const loginAPI = data => request.post('/api/login', data)
Login.vue 组件中,导入 loginAPI,调用它,发送ajax请求
methods: {
login() {
this.$refs.form.validate(async valid => {
if (!valid) return // 表单验证未通过,直接 return
// 表单验证通过,发送请求,完成登录
// console.log('通过验证')
const { data: res } = await loginAPI(this.userInfo)
if (res.code === 0) {
this.$message.success(res.message)
} else {
this.$message.error(res.message)
}
})
}
}
this.$router.push('/')
)在store文件夹,新建 user.js
export default {
namespaced: true,
state() {
return {
token: ''
}
},
mutations: {
// 修改token,只能通过下面的方法修改
updateToken(state, token) {
state.token = token
}
},
actions: {},
getters: {}
}
在store文件夹的 index.js 中,导入 user.js 并注册成模块
import Vue from 'vue'
import Vuex from 'vuex'
+import userModule from './user.js'
Vue.use(Vuex)
export default new Vuex.Store({
+ strict: true,
modules: {
+ // 模块名:导入进来的值
+ user: userModule
}
})
Login.vue 组件中,登录成功后,调用 mutations中的方法,对token进行更新
if (res.code === 0) {
this.$message.success(res.message)
// 存储token(调用mutations中的updateToken方法)
// this.$store.commit('模块名/方法名', '其他参数')
this.$store.commit('user/updateToken', res.token)
// 跳转到后台首页
this.$router.push('/')
} else {
this.$message.error(res.message)
}
测试
目前,token存储到vuex当中,即内存中。
页面只要一刷新,就会重置token为空。
所以需要 vuex-persistedstate
插件,该插件可以
插件的参考网站:https://www.npmjs.com/package/vuex-persistedstate
具体使用:
npm i vuex-persistedstate@3.2.1
store/index.js中,导入并注册为插件
import Vue from 'vue'
import Vuex from 'vuex'
+import createPersistedState from 'vuex-persistedstate'
import userModule from './user.js'
Vue.use(Vuex)
export default new Vuex.Store({
strict: true,
+ plugins: [createPersistedState()],
modules: {
// 模块名:导入进来的值
user: userModule
}
})
router/index.js 中,增加 /
路由规则,指定显示 Main.vue组件
const routes = [
// 直接写到这个最大的数组中的路由,对应的组件,应该显示在App.vue的占位符位置
// 比如下面的 登录组件,应该显示在App.vue的占位符位置
// { path: '路由地址', component: '组件' }
{ path: '/login', component: Login }, // 使用组件应该先导入
{ path: '/reg', component: () => import('@/views/Reg.vue') },
// 使用 / 当前后台首页
+ { path: '/', component: () => import('@/views/Main.vue') }
]
在views文件夹,创建对应的组件 Main.vue
参考文档:https://element.eleme.cn/#/zh-CN/component/container
<template>
<el-container class="main-container">
<el-header>Header</el-header>
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-container>
<el-main>Main</el-main>
<el-footer>Footer</el-footer>
</el-container>
</el-container>
</el-container>
</template>
<style lang="less">
.main-container {
height: 100%;
}
.el-header,
.el-aside {
background-color: #23262e;
}
.el-main {
background-color: #f2f2f2;
}
.el-footer {
background-color: #eee;
}
</style>
一个菜单就是一个 <el-menu>.....</el-menu>
如果只是一级菜单,没有下拉菜单,则使用 <el-menu-item index="1">一个菜单</el-menu-item>
如果有下拉菜单,则使用
<el-submenu index="2">
<template slot="title">顶级菜单</template>
<el-menu-item index="2-1">子菜单1</el-menu-item>
<el-menu-item index="2-2">子菜单2</el-menu-item>
<el-menu-item index="2-3">子菜单3</el-menu-item>
</el-submenu>
菜单属性(<el-menu>
标签的属性)
子菜单的属性(<el-submenu>
和 <el-menu-item>
的属性)
完整的HTML
<template>
<el-container class="main-container">
<el-header>
<img src="../assets/images/logo.png" alt="" />
<!-- 头部 右侧的菜单 -->
<el-menu
mode="horizontal"
background-color="#23262e"
text-color="#fff"
active-text-color="#409eff"
>
<!-- 下面的菜单没有下拉菜单,则使用 el-menu-item 标签 -->
<!-- <el-menu-item>处理中心</el-menu-item> -->
<!-- 下面的菜单有下拉菜单,则使用 el-submenu 标签 -->
<el-submenu index="1">
<template slot="title">
<img class="avatar" src="../assets/logo.png" alt="" />
个人中心
</template>
<el-menu-item index="1-1"><i class="el-icon-edit"></i>用户资料</el-menu-item>
<el-menu-item index="1-2"><i class="el-icon-edit"></i>重置密码</el-menu-item>
<el-menu-item index="1-3"><i class="el-icon-edit"></i>更换头像</el-menu-item>
</el-submenu>
<el-menu-item index="2"><i class="el-icon-switch-button"></i>退出</el-menu-item>
</el-menu>
</el-header>
<el-container>
<el-aside width="200px">
<div class="user-info">
<img class="avatar" src="../assets/logo.png" alt="" />
<span>欢迎你 xxx</span>
</div>
<!-- 侧边栏的菜单 -->
<el-menu
background-color="#23262e"
text-color="#fff"
active-text-color="#409eff"
default-active="aaa"
unique-opened
class="aside-menu"
>
<el-menu-item index="aaa"><i class="el-icon-edit"></i>首页</el-menu-item>
<el-submenu index="abc">
<template slot="title"><i class="el-icon-edit"></i>文章管理</template>
<el-menu-item index="bbb"><i class="el-icon-edit"></i>文章分类</el-menu-item>
<el-menu-item index="ccc"><i class="el-icon-edit"></i>文章列表</el-menu-item>
</el-submenu>
<el-submenu index="bcd">
<template slot="title"><i class="el-icon-edit"></i>个人中心</template>
<el-menu-item index="ddd"><i class="el-icon-edit"></i>基本资料</el-menu-item>
<el-menu-item index="eee"><i class="el-icon-edit"></i>重置密码</el-menu-item>
<el-menu-item index="fff"><i class="el-icon-edit"></i>更换头像</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<el-main>Main</el-main>
<el-footer>Footer</el-footer>
</el-container>
</el-container>
</el-container>
</template>
首页四个区域的样式
.main-container {
height: 100%;
}
.el-header,
.el-aside {
background-color: #23262e;
}
.el-main {
background-color: #f2f2f2;
}
.el-footer {
background-color: #eee;
}
调整头部区,使用flex布局,让logo图片和菜单左右分散对齐
.el-header {
display: flex;
justify-content: space-between;
align-items: center;
}
侧边栏的样式
.aside-menu {
width: 200px;
}
.avatar {
width: 35px;
height: 35px;
background-color: #fff;
border-radius: 50%;
margin-right: 15px;
// object-fit: cover 和 background-size基本一个意思
// 表示让图片按照原始的比例完整的显示出来
object-fit: cover;
}
.user-info {
height: 70px;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
}
自愿 在 main.js 中导入 assets 里面的 global.less 。
如果导入,需要将 global.less 中 “重置卡片的基础样式” 中的 font-size: 13px 去掉
/my/userinfo
发送请求/my/userinfo
发送请求,就需要设置请求头 Authorization: token字符串
具体的代码
utils/request.js 代码如下:(注意,拦截器要加给 request 对象)
import axios from 'axios'
// 导入 store/index.js ,因为要使用这里的state中的token
import store from '@/store'
// 创建axios的实例。 这个实例中,可以对axios进行全局的配置
// 得到的 request 也是axios。只不过名字不能再叫axios,因为重名了
const request = axios.create({
baseURL: 'http://www.liulongbin.top:3008'
})
// 通过请求拦截器来设置请求头
// 添加请求拦截器
request.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
// config就是请求的配置对象
/**
* config = {
* url: '/user',
* method: 'get',
* baseURL: 'https://some-domain.com/api/',
* headers: {'X-Requested-With': 'XMLHttpRequest'},
* ......
* }
*/
// 添加请求头,判断一下,如果请求的url地址,是以 /my 开头的,则添加请求头
if (config.url.startsWith('/my/')) {
// config.headers.Authorization = 'tokenxxxxxx'
// 组件中的this.$store ==== 上面导入的store
config.headers.Authorization = store.state.user.token
}
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// const request2 = axios.create({
// baseURL: 'http://www.itcbc.com'
// })
export default request
注意,拦截器要加给 request 对象
由于前面已经给 request 加好了拦截器。所以下面直接发送请求即可
api/reg.js中代码
// .....其他代码略
// 导出获取用户信息的方法
export const getUserAPI = () => request.get('/my/userinfo')
具体代码
store/user.js
// 导入获取用户信息的方法
import { getUserAPI } from '@/api/reg'
export default {
namespaced: true,
state() {
return {
token: '',
user: {} // 准备存储用户数据
}
},
mutations: {
// 修改token,只能通过下面的方法修改
updateToken(state, token) {
state.token = token
},
// 更新user数据
updateUser(state, user) {
state.user = user
}
},
actions: {
async getUser(ctx) {
const { data: res } = await getUserAPI()
// console.log(res)
if (res.code === 0) {
ctx.commit('updateUser', res.data)
}
}
},
getters: {}
}
上述方法并不会自动执行,所以在组件的生命周期函数中,调用 actions中的 getUser 方法。
Main.vue中,调用
export default {
created() {
// 调用vuex中的 actions 中的 getUser 方法
// this.$store.dispatch('模块名/方法名')
this.$store.dispatch('user/getUser')
}
}
经过上述步骤,数据在登录后,即可存储到 vuex 当中。
Main.vue中,调用
import { mapState } from 'vuex'
export default {
created() {
// 调用vuex中的 actions 中的 getUser 方法
// this.$store.dispatch('模块名/方法名')
this.$store.dispatch('user/getUser')
},
computed: {
// ...mapState('模块名', ['xxx', 'xxxx'])
...mapState('user', ['user'])
}
}
页面中:
<div class="user-info">
<img class="avatar" :src="user.user_pic" alt="" v-if="user.user_pic" />
<img class="avatar" src="../assets/logo.png" alt="" v-else />
<!-- 有昵称nickname,则使用昵称,没有昵称,只能使用账号username -->
<span>欢迎你 {{ user.nickname || user.username }}</span>
</div>
前提,先手动清除本地存储的vuex,刷新页面,检测vuex中的token是否为空
如果vuex中的token为空,则不允许访问后台首页 (/)。此时可以使用全局前置导航守卫进行控制
// 必须加载store,因为下面的判断使用了vuex中的数据
import store from '@/store'
// 全局前置导航守卫(判断token是否为空,如果token为空,则跳转到登录页,不允许访问其他页面(路由))
router.beforeEach((to, from, next) => {
// 判断访问的是否是登录或注册,如果是,则不需要判断token,直接放行
// console.log(to) // to.path 表示即将访问的那个路由
if (to.path === '/login' || to.path === '/reg') {
next()
} else {
if (store.state.user.token) {
next() // 表示允许访问(放行)
} else {
next('/login') // 表示跳转到指定的路由
}
}
})
前面的导航守卫只能判断token有没有,但是浏览器端无法判断token的真假,无法判断token是否过期 只能使用服务端响应结果来判断token的真假
具体做法
代码中,使用了 store 和 router,所以一定要检测代码中是否加载的 '@/store' 和 '@/router'
// 添加响应拦截器,判断token的真假
request.interceptors.response.use(
function (response) {
// 对响应数据做点什么(响应状态码如果是2xx,则执行这个函数)
return response
},
function (error) {
// 对响应错误做点什么(响应状态码超过 2xx的范围,则执行这个函数)
// console.dir(error) // 使用 dir 的目的是能够展开错误对象,能够看到对象内部的结构
if (error.response) {
if (error.response.data.code === 1 && error.response.data.message === '身份认证失败!') {
// 说明浏览器端的token是假的或者是过期的
// 加个提示
// 清除假token
store.commit('user/updateToken', '')
// 清除用户数据
store.commit('user/updateUser', {})
// 跳转到登录页
router.push('/login')
}
}
return Promise.reject(error)
}
)
@click="logout"
logout
方法methods: {
logout() {
// 询问是否要退出
this.$confirm('确定要退出吗?', '提示')
.then(() => {
// 点击了确定
// console.log(111)
// 确定退出,清除token、清除user
this.$store.commit('user/updateToken', '')
this.$store.commit('user/updateUser', {})
// 跳转到登录页
this.$router.push('/login')
})
.catch(() => {
// 点击了取消
// console.log(22)
})
}
}
menu.js 代码
// 菜单相关的请求
import request from '@/utils/request'
export const getMenuAPI = () => request.get('/my/menus')
Main.vue 代码
export default {
data() {
return {
menus: []
}
},
async created() {
// 调用vuex中的 actions 中的 getUser 方法
// this.$store.dispatch('模块名/方法名')
this.$store.dispatch('user/getUser')
// 调用 getMenuAPI 方法,马上获取菜单数据
const { data: res } = await getMenuAPI()
// console.log(res)
if (res.code === 0) {
// 获取菜单成功,把数据存储到哪里?
this.menus = res.data
}
},
...............
}
<template>
标签,v-for 加个它v-if="item.children === null"
来控制,到达使用el-menu-item 标签 还是 el-submenu 标签:key="item.indexPath"
不能加给虚拟的 template 标签:
<template v-for="item in menus">
<el-menu-item
:index="item.indexPath"
v-if="item.children === null"
:key="item.indexPath"
>
<i :class="item.icon"></i>{{ item.title }}
</el-menu-item>
<el-submenu :index="item.indexPath" v-else :key="item.indexPath">
<template slot="title"><i :class="item.icon"></i>{{ item.title }}</template>
<el-menu-item index="bbb"><i class="el-icon-edit"></i>文章分类</el-menu-item>
<el-menu-item index="ccc"><i class="el-icon-edit"></i>文章列表</el-menu-item>
</el-submenu>
</template>
<el-submenu :index="item.indexPath" v-else :key="item.indexPath">
<template slot="title"><i :class="item.icon"></i>{{ item.title }}</template>
<el-menu-item
:index="subItem.indexPath"
v-for="subItem in item.children"
:key="subItem.indexPath"
>
<i :class="subItem.icon"></i>{{ subItem.title }}
</el-menu-item>
</el-submenu>
<template>
<div>这是首页 --- 这里放echarts图表</div>
</template>
{
path: '/',
redirect: '/home',
component: () => import('@/views/Main.vue'),
children: [
{ path: 'art-cate', component: () => import('@/views/ArtCate.vue') },
{ path: 'home', component: () => import('@/views/Home.vue') }
]
}
<router-view></router-view>
npm i echarts
下载安装echarts<template>
<div>
<div class="container-fluid">
<el-row class="spannel_list" :gutter="10">
<el-col :sm="6" :xs="12">
<div class="spannel">
<em>10015</em><span>篇</span>
<b>总文章数</b>
</div>
</el-col>
<el-col :sm="6" :xs="12">
<div class="spannel scolor01">
<em>123</em><span>篇</span>
<b>日新增文章数</b>
</div>
</el-col>
<el-col :sm="6" :xs="12">
<div class="spannel scolor02">
<em>35</em><span>条</span>
<b>评论总数</b>
</div>
</el-col>
<el-col :sm="6" :xs="12">
<div class="spannel scolor03">
<em>123</em><span>条</span>
<b>日新增评论数</b>
</div>
</el-col>
</el-row>
</div>
<div class="container-fluid">
<el-row class="curve-pie" :gutter="10">
<el-col :sm="16" :xs="16">
<div class="gragh_pannel" id="curve_show"></div>
</el-col>
<el-col :sm="8" :xs="8">
<div class="gragh_pannel" id="pie_show"></div>
</el-col>
</el-row>
</div>
<div class="container-fluid">
<div class="column_pannel" id="column_show"></div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
export default {
name: 'Home',
mounted() {
var oChart = echarts.init(document.getElementById('curve_show'))
var aListAll = [
{ count: 36, date: '2019-04-13' },
{ count: 52, date: '2019-04-14' },
{ count: 78, date: '2019-04-15' },
{ count: 85, date: '2019-04-16' },
{ count: 65, date: '2019-04-17' },
{ count: 72, date: '2019-04-18' },
{ count: 88, date: '2019-04-19' },
{ count: 64, date: '2019-04-20' },
{ count: 72, date: '2019-04-21' },
{ count: 90, date: '2019-04-22' },
{ count: 96, date: '2019-04-23' },
{ count: 100, date: '2019-04-24' },
{ count: 102, date: '2019-04-25' },
{ count: 110, date: '2019-04-26' },
{ count: 123, date: '2019-04-27' },
{ count: 100, date: '2019-04-28' },
{ count: 132, date: '2019-04-29' },
{ count: 146, date: '2019-04-30' },
{ count: 200, date: '2019-05-01' },
{ count: 180, date: '2019-05-02' },
{ count: 163, date: '2019-05-03' },
{ count: 110, date: '2019-05-04' },
{ count: 80, date: '2019-05-05' },
{ count: 82, date: '2019-05-06' },
{ count: 70, date: '2019-05-07' },
{ count: 65, date: '2019-05-08' },
{ count: 54, date: '2019-05-09' },
{ count: 40, date: '2019-05-10' },
{ count: 45, date: '2019-05-11' },
{ count: 38, date: '2019-05-12' }
]
const aCount = []
const aDate = []
for (var i = 0; i < aListAll.length; i++) {
aCount.push(aListAll[i].count)
aDate.push(aListAll[i].date)
}
var chartopt = {
title: {
text: '月新增文章数',
left: 'center',
top: '10'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['新增文章'],
top: '40'
},
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
magicType: { show: true, type: ['line', 'bar'] },
restore: { show: true },
saveAsImage: { show: true }
}
},
calculable: true,
xAxis: [
{
name: '日',
type: 'category',
boundaryGap: false,
data: aDate
}
],
yAxis: [
{
name: '月新增文章数',
type: 'value'
}
],
series: [
{
name: '新增文章',
type: 'line',
smooth: true,
areaStyle: { type: 'default' },
itemStyle: { color: '#f80', lineStyle: { color: '#f80' } },
data: aCount
}
],
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(255,136,0,0.39)'
},
{
offset: 0.34,
color: 'rgba(255,180,0,0.25)'
},
{
offset: 1,
color: 'rgba(255,222,0,0.00)'
}
])
}
},
grid: {
show: true,
x: 50,
x2: 50,
y: 80,
height: 220
}
}
oChart.setOption(chartopt)
var oPie = echarts.init(document.getElementById('pie_show'))
var oPieopt = {
title: {
top: 10,
text: '分类文章数量比',
x: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
color: ['#5885e8', '#13cfd5', '#00ce68', '#ff9565'],
legend: {
x: 'center',
top: 65,
data: ['奇趣事', '会生活', '爱旅行', '趣美味']
},
toolbox: {
show: true,
x: 'center',
top: 35,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
magicType: {
show: true,
type: ['pie', 'funnel'],
option: {
funnel: {
x: '25%',
width: '50%',
funnelAlign: 'left',
max: 1548
}
}
},
restore: { show: true },
saveAsImage: { show: true }
}
},
calculable: true,
series: [
{
name: '访问来源',
type: 'pie',
radius: ['45%', '60%'],
center: ['50%', '65%'],
data: [
{ value: 300, name: '奇趣事' },
{ value: 100, name: '会生活' },
{ value: 260, name: '爱旅行' },
{ value: 180, name: '趣美味' }
]
}
]
}
oPie.setOption(oPieopt)
var oColumn = echarts.init(document.getElementById('column_show'))
var oColumnopt = {
title: {
text: '文章访问量',
left: 'center',
top: '10'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['奇趣事', '会生活', '爱旅行', '趣美味'],
top: '40'
},
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
magicType: { show: true, type: ['line', 'bar'] },
restore: { show: true },
saveAsImage: { show: true }
}
},
calculable: true,
xAxis: [
{
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月']
}
],
yAxis: [
{
name: '访问量',
type: 'value'
}
],
series: [
{
name: '奇趣事',
type: 'bar',
barWidth: 20,
areaStyle: { type: 'default' },
itemStyle: {
color: '#fd956a'
},
data: [800, 708, 920, 1090, 1200]
},
{
name: '会生活',
type: 'bar',
barWidth: 20,
areaStyle: { type: 'default' },
itemStyle: {
color: '#2bb6db'
},
data: [400, 468, 520, 690, 800]
},
{
name: '爱旅行',
type: 'bar',
barWidth: 20,
areaStyle: { type: 'default' },
itemStyle: {
color: '#13cfd5'
},
data: [500, 668, 520, 790, 900]
},
{
name: '趣美味',
type: 'bar',
barWidth: 20,
areaStyle: { type: 'default' },
itemStyle: {
color: '#00ce68'
},
data: [600, 508, 720, 890, 1000]
}
],
grid: {
show: true,
x: 50,
x2: 30,
y: 80,
height: 260
},
dataZoom: [
// 给x轴设置滚动条
{
start: 0, // 默认为0
end: 100 - 1000 / 31, // 默认为100
type: 'slider',
show: true,
xAxisIndex: [0],
handleSize: 0, // 滑动条的 左右2个滑动条的大小
height: 8, // 组件高度
left: 45, // 左边的距离
right: 50, // 右边的距离
bottom: 26, // 右边的距离
handleColor: '#ddd', // h滑动图标的颜色
handleStyle: {
borderColor: '#cacaca',
borderWidth: '1',
shadowBlur: 2,
background: '#ddd',
shadowColor: '#ddd'
}
}
]
}
oColumn.setOption(oColumnopt)
}
}
</script>
<style lang="less" scoped>
.spannel_list {
margin-top: 20px;
}
.spannel {
height: 100px;
overflow: hidden;
text-align: center;
position: relative;
background-color: #fff;
border: 1px solid #e7e7e9;
margin-bottom: 20px;
}
.spannel em {
font-style: normal;
font-size: 50px;
line-height: 50px;
display: inline-block;
margin: 10px 0 0 20px;
font-family: 'Arial';
color: #83a2ed;
}
.spannel span {
font-size: 14px;
display: inline-block;
color: #83a2ed;
margin-left: 10px;
}
.spannel b {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
line-height: 24px;
background: #e5e5e5;
color: #333;
font-size: 14px;
font-weight: normal;
}
.scolor01 em,
.scolor01 span {
color: #6ac6e2;
}
.scolor02 em,
.scolor02 span {
color: #5fd9de;
}
.scolor03 em,
.scolor03 span {
color: #58d88e;
}
.gragh_pannel {
height: 350px;
border: 1px solid #e7e7e9;
background-color: #fff !important;
margin-bottom: 20px;
}
.column_pannel {
margin-bottom: 20px;
height: 400px;
border: 1px solid #e7e7e9;
background-color: #fff !important;
}
</style>
随时项目的开发,组件会越来越多。如果把组件全部放到 views 中,会非常乱,不好找 正确的做法是,将组件分门别类的存储在子文件夹中。 比如我们的项目,菜单:
菜单(Menu)
- 首页(Home)
- Home.vue
- 文章管理(Article)
- 文章分类 XXX.vue
- 文章列表 XXX.vue
- 个人中心(User)
- 用户资料 UserInfo.vue
- 重置密码 Xxxxx.vue
- 更换头像 Xxxx.vue
目前,我们开发到用户资料这个阶段,所以在 /views/Menu/User 中 创建 UserInfo.vue 并且把Home.vue的位置修改一下。 最后,路由的配置:
{
path: '/',
redirect: '/home',
component: () => import('@/views/Main.vue'),
children: [
{ path: 'home', component: () => import('@/views/Menu/Home/Home.vue') },
{ path: 'user-info', component: () => import('@/views/Menu/User/UserInfo.vue') }
]
}
elementUI的卡片和表单
<template>
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>基本资料</span>
</div>
<div class="text item">
<el-form ref="form" label-width="80px">
<!-- 第一项:登录账号 -->
<el-form-item label="登录账号">
<el-input disabled></el-input>
</el-form-item>
<!-- 第二项:用户昵称 -->
<el-form-item label="用户昵称">
<el-input></el-input>
</el-form-item>
<!-- 第三项:用户邮箱 -->
<el-form-item label="用户邮箱">
<el-input></el-input>
</el-form-item>
<!-- 第四项:按钮 -->
<el-form-item>
<el-button type="primary">立即创建</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</template>
<style lang="less" scoped>
.el-card {
font-size: 13px;
}
.el-form {
width: 500px;
}
</style>
做修改之前,必须进行数据回填(把当前用户的数据,回填到输入框中)
怎样数据回填?
v-model
进行双向数据绑定export default {
data() {
return {
userInfo: {
username: this.$store.state.user.user.username,
nickname: this.$store.state.user.user.nickname,
email: this.$store.state.user.user.email
}
}
}
}
每个 input 框,加好 v-model
如果数据回填后,发现昵称和邮箱是空的,不一定是错的。新注册的账号,就是没有昵称和邮箱的
<el-form-item prop="字段">
)formRules: { 字段: [{required: true}, {...}], 字段: [] }
{ type: 'email', message: '邮箱格式不正确', trigger: ['change', 'blur'] }
this.$refs.表单的ref值.validate(valid => { /* 根据valid进行判断 */ })
let obj1 = { name: 'zs', age: 20 };
// let obj2 = obj1;
// 浅拷贝和深拷贝
// 浅拷贝,把一个对象的属性和值,拷贝一份,加入到另一个空对象中
// 深拷贝,把一个对象的属性和值,拷贝一份,加入到另一个对象。(如果对象的某个属性值是引用类型的,则需要递归拷贝),拷贝后,两个对象是完全独立的两个对象
// 浅拷贝的方案(for循环、Object.assign、转成JSON再转成JS对象、...展开运算符)
let obj2 = {};
// for (let k in obj1) {
// obj2[k] = obj1[k]
// }
// obj2 = Object.assign({}, obj1)
// obj2 = JSON.parse(JSON.stringify(obj1))
obj2 = { ...obj1 }
项目中,可以通过浅拷贝来简化代码:
UserInfo.vue 中:
data() {
return {
userInfo: { ...this.$store.state.user.user }
}
}
<el-button type="primary" @click="updateUser">确认修改</el-button>
methods: {
async updateUser() {
// console.log(this.userInfo)
const { data: res } = await updateUserAPI(this.userInfo)
// console.log(res)
if (res.code === 0) {
this.$message.success(res.message)
// 调用 vuex 中的 actions 中的 getUser 方法,获取并更新最新的用户数据
// this.$store.dispatch('模块名/方法名')
this.$store.dispatch('user/getUser')
} else {
this.$message.error(res.message)
}
}
}
// 更新用户信息
export const updateUserAPI = data => request.put('/my/userinfo', data)
修改 按钮 为 重置两个字
加入单击事件 <el-button @click="reset">重置</el-button>
补充 reset 方法
reset() {
// 下面的代码作用是重置表单(恢复输入框的值为打开页面时的初始值;清除掉表单验证红色的提示)
this.$refs.userInfoRef.resetFields()
// 从vuex中更新一份数据到当前组件的 userInfo中
this.userInfo = Object.assign({}, this.$store.state.user.user)
}
因为页面和用户资料页面一样。所以复制“用户资料”页面的结构
配置路由 (router/index.js)
{
path: '/',
redirect: '/home',
component: () => import('@/views/Main.vue'),
children: [
{ path: 'home', component: () => import('@/views/Menu/Home/Home.vue') },
{ path: 'user-info', component: () => import('@/views/Menu/User/UserInfo.vue') },
+ { path: 'user-pwd', component: () => import('@/views/Menu/User/RePwd.vue') }
]
}
<el-form :model="数据项" :rules="验证规则">
每个 <el-form-item>
加入 prop
属性
每个 <el-input>
加入 v-model
双向数据绑定
定义具体的规则
自定义验证规则(验证新密码不能和原密码一致;验证两次新密码必须一致)
在 data 中,return之前,声明函数
const xxx = (rule, value, callback) => {
// rule 表示验证规则对象,没有什么用
// value 表示当前输入框的值 (比如新密码使用了这个规则,则value表示输入的新密码)
// callback() --- 表示通过验证
// callbakc(new Error('不通过的提示')) --- 表示验证不通过
}
验证规则中,加入这个自定义的规则
new_pwd: [
{ required: true, message: '原密码必填', trigger: 'blur' },
{ pattern: /^\S{6,15}$/, message: '6~15位的非空字符', trigger: 'blur' },
+ { validator: xxx, trigger: 'blur' }
]
提交密码,需要发送请求,所以需要一个API方法 (api/reg.js)
// 重置密码
export const updatePwdAPI = data => request.patch('/my/updatepwd', data)
表单提交,注册按钮的单击事件
<el-button type="primary" @click="updatePwd">确认修改</el-button>
事件处理方法中,找到表单进行一次完整的验证
验证通过,则调用 API 方法,发送请求
更新成功,提示,重置表单
import { updatePwdAPI } from '@/api/reg.js'
methods: {
updatePwd() {
this.$refs.passRef.validate(async valid => {
if (valid) {
// 表示验证通过
const { data: res } = await updatePwdAPI(this.pass)
if (res.code === 0) {
this.$message.success(res.message) // 提示
this.$refs.passRef.resetFields() // 重置表单
} else {
this.$message.error(res.message)
}
}
})
}
}
<el-button @click="$refs.passRef.resetFields()">重置</el-button>
创建组件,并复制页面结构
<template>
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>更换头像</span>
</div>
<div>
<!-- 图片,用来展示用户选择的头像 -->
<img src="../../../assets/images/avatar.jpg" alt="" class="preview" />
<!-- 按钮区域 -->
<div class="btn-box">
<el-button type="primary" icon="el-icon-plus">选择图片</el-button>
<el-button type="success" icon="el-icon-upload" disabled>上传头像</el-button>
</div>
</div>
</el-card>
</template>
<script>
export default {
name: 'UserAvatar'
}
</script>
<style lang="less" scoped>
.btn-box {
margin-top: 10px;
}
.preview {
object-fit: cover;
width: 350px;
height: 350px;
}
</style>
配置路由
{
path: '/',
redirect: '/home',
component: () => import('@/views/Main.vue'),
children: [
{ path: 'home', component: () => import('@/views/Menu/Home/Home.vue') },
{ path: 'user-info', component: () => import('@/views/Menu/User/UserInfo.vue') },
{ path: 'user-pwd', component: () => import('@/views/Menu/User/RePwd.vue') },
+ { path: 'user-avatar', component: () => import('@/views/Menu/User/Avatar.vue') }
]
}
按钮前面,添加一个文件域 <input type="file" ref="fileRef" />
,因为在所有的HTML标签中,只有文件域能够选择图片。
点击按钮,触发文件域的单击事件即可。
<el-button type="primary" icon="el-icon-plus" @click="$refs.fileRef.click()">
选择图片
</el-button>
无论使用 FileReader 对象,还是使用 URL对象做预览效果,都需要先得到文件对象。
如何得到文件对象?
找到文件域,注册 change 事件 (当文件域的文件改变的时候,触发change事件)
<input type="file" ref="fileRef" @change="preview" />
事件源.files[0] 就是文件对象
methods: {
preview(e) {
// e.target // 表示事件源
// console.dir(e.target)
if (e.target.files.length > 0) {
// 说明我们选择了图片
// 获取文件对象
const fileObj = e.target.files[0]
console.log(fileObj)
} else {
// 说明没有选择图片
}
}
}
使用URL对象,实现图片预览
https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL
调用浏览器内置方法 URL.createObjectURL
,参数是 文件对象。返回值就是一个url。这个url可以访问到我们选择的那个图片
const url = URL.createObjectURL(fileObj)
把url转存到data数据项中
export default {
data() {
return {
url: ''
}
},
methods: {
preview(e) {
// e.target // 表示事件源
// console.dir(e.target)
if (e.target.files.length > 0) {
// 说明我们选择了图片
// 获取文件对象
const fileObj = e.target.files[0]
// console.log(fileObj)
// 得到访问图片的url地址
const url = URL.createObjectURL(fileObj)
// console.log(url)
this.url = url
} else {
// 说明没有选择图片
}
}
}
}
页面中,展示
<!-- 图片,用来展示用户选择的头像 -->
<img src="../../../assets/images/avatar.jpg" alt="" class="preview" v-if="url === ''" />
<img :src="url" alt="" class="preview" v-else />
使用FileReader实现图片预览
https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsDataURL
实例化 FileReader对象
调用 readAsDataURL(文件对象)
方法,读取并转换成base64
通过 onload
事件,监听转换是否完成
转换完成后,通过 result
属性获取结果
methods: {
preview(e) {
// e.target // 表示事件源
// console.dir(e.target)
if (e.target.files.length > 0) {
// 说明我们选择了图片
// 获取文件对象
const fileObj = e.target.files[0]
// console.log(fileObj)
// 通过 FileReader 得到base64格式
const fr = new FileReader()
fr.readAsDataURL(fileObj)
// 下面的事件处理函数,必须使用箭头函数。否则this就表示事件源fr了
fr.onload = () => {
// 把转成后的base64结果,赋值给数据项中的url
this.url = fr.result
}
} else {
// 说明没有选择图片
}
}
}
页面中的 img 标签
<!-- 图片,用来展示用户选择的头像 -->
<img src="../../../assets/images/avatar.jpg" alt="" class="preview" v-if="url === ''" />
<img :src="url" alt="" class="preview" v-else />
封装 更换头像的 API 方法
// 更换头像
export const updateAvatarAPI = str => {
return request.patch('/my/update/avatar', { avatar: str })
}
根据数据是否为空,控制 按钮是否 为禁用
注册单击事件
<el-button
type="success"
icon="el-icon-upload"
:disabled="url === ''"
@click="updateAvatar"
>
上传头像
</el-button>
事件处理方法,调用API方法,发送请求,提交 base64 字符串
更换成功后,调用 vuex 中的 actions 中的 getUser
方法,更新数据
async updateAvatar() {
const { data: res } = await updateAvatarAPI(this.url)
if (res.code === 0) {
this.$message.success(res.message)
// 调用 vuex 中的 actions中的 getUser 方法,更新用户数据
this.$store.dispatch('user/getUser')
} else {
this.$message.error(res.message)
}
}
创建 在 /views/Menus/Article 中,创建 ArtCate.vue,代码如下
<template>
<el-card class="box-card">
<div slot="header" class="clearfix card-header">
<span>文章分类</span>
<el-button type="primary" size="medium">添加分类</el-button>
</div>
<!-- 卡片的内容区,放表格 -->
<el-table>
<!-- 第1列 prop表示数据项的名字; label是表头 -->
<el-table-column prop="date" label="序号" width="100"> </el-table-column>
<el-table-column prop="date" label="分类名称"> </el-table-column>
<el-table-column prop="date" label="分类别名"> </el-table-column>
<el-table-column prop="date" label="操作"> </el-table-column>
</el-table>
</el-card>
</template>
<script>
export default {}
</script>
<style lang="less" scoped>
.el-card {
font-size: 13px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
配置路由 (router/index.js)
{ path: 'art-cate', component: () => import('@/views/Menu/Article/ArtCate.vue') }
api/article.js 封装API方法,发送请求获取文章分类数据
import request from '@/utils/request'
// 封装获取文章类别的API方法
export const getCategoryAPI = () => request.get('/my/cate/list')
组件中,调用API方法,取得文章分类数据。
把数据转存到data数据项中
// 按需加载 API 方法
import { getCategoryAPI } from '@/api/article.js'
export default {
data() {
return {
cateList: []
}
},
created() {
// 页面刷新,马上调用获取文章分类的方法
this.initCategory()
},
methods: {
async initCategory() {
const { data: res } = await getCategoryAPI()
if (res.code === 0) {
this.cateList = res.data
}
}
}
}
配合element-ui的语法,将数据渲染到表格中
<el-table>
加入 :data="数据项"
<el-table-column>
加入 prop="字段"
type=index
<el-table :data="cateList">
<!-- 第1列 prop表示数据项的名字; label是表头 -->
<!-- 第一列放序号,使用固定的 type="index" 即可得到连续的序号 -->
<el-table-column type="index" label="序号" width="100"> </el-table-column>
<el-table-column prop="cate_name" label="分类名称"> </el-table-column>
<el-table-column prop="cate_alias" label="分类别名"> </el-table-column>
<el-table-column label="操作"> </el-table-column>
</el-table>
表格如果需要垂直方向的边框,给
<el-table>
加入border
属性表格如果需要隔行换色,给
<el-table>
加入stripe
属性
点击“添加分类”,显示对话框
addVisible: false
@click="addVisible=true"
addVisible
<el-dialog title="提示" :visible.sync="addVisible" width="30%">
<span>这是一段信息</span>
<span slot="footer" class="dialog-footer">
<el-button @click="addVisible = false">取 消</el-button>
<el-button type="primary" @click="addVisible = false">确 定</el-button>
</span>
</el-dialog>
绘制弹框中的表单(把原来的 <span>这是一段信息</span>
换成表单)
<el-form label-width="80px">
<!-- 第一项:分类名称 -->
<el-form-item label="分类名称">
<el-input></el-input>
</el-form-item>
<!-- 第二项:分类别名 -->
<el-form-item label="分类别名">
<el-input></el-input>
</el-form-item>
</el-form>
表单验证
给 <el-form>
加入 :model="数据项"
和 :rules="验证规则"
给 <el-form-item>
加入 prop="字段"
给 <el-input>
加入 v-model="字段"
JS中,设置数据项,设置验证规则
data() {
return {
cateList: [],
addVisible: false, // 控制添加的弹框是否显示,默认false。表示不显示
// 添加分类的数据项
addData: {
cate_name: '', // 分类名称
cate_alias: '' // 分类别名
},
// 添加分类的验证规则
addFormRules: {
cate_name: [
{ required: true, message: '分类名称必填', trigger: 'blur' },
{ pattern: /^\S{1,10}$/, message: '分类名称必须是1~10位非空字符', trigger: 'blur' }
],
cate_alias: [
{ required: true, message: '分类别名必填', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9]{1,15}$/,
message: '分类名称必须是1~15位的字母数字组合',
trigger: 'blur'
}
]
}
}
},
提交表单数据,完成添加
封装 API 方法
// 封装添加类别的方法
export const addCategoryAPI = data => request.post('/my/cate/add', data)
组件中,修改确定按钮的单击事件,单击的时候,调用方法
<el-button type="primary" @click="addCategory">确 定</el-button>
补充添加分类的方法。
// 添加类别的方法
addCategory() {
this.$refs.addRef.validate(async valid => {
if (valid) {
const { data: res } = await addCategoryAPI(this.addData)
if (res.code === 0) {
this.$message.success(res.message) // 提示
this.$refs.addRef.resetFields() // 重置表单
this.addVisible = false // 关闭弹框
this.initCategory() // 更新数据
} else {
this.$message.error(res.message)
}
}
})
}
当弹框关闭的时候,应该重置一下表单
给 <el-dialog>
加入 :before-close="resetAddForm"
,表示关闭弹框之前,做什么
JS中,补充 resetAddForm
方法
// 关闭添加弹框的时候,重置表单
resetAddForm(done) {
this.$refs.addRef.resetFields() // 重置表单
this.addVisible = false // 关闭弹框
// done() // 调用它,表示关闭对话框(必须调用它,如果不调用,会暂停 Dialog 的关闭)
},
取消的时候,也调用这个方法
<el-button @click="resetAddForm">取 消</el-button>
封装删除分类的API方法
// 删除分类
export const deleteCategoryAPI = id => request.delete('/my/cate/del', { params: { id } })
参考elementUI的表格 https://element.eleme.cn/#/zh-CN/component/table
找到带有“编辑” 或 “删除” 的表格,分析
发现 操作列使用了vue的 “作用域插槽” (通过 slot-scope="scope" 接收到数据)
slot-scope
是vue2版本的插槽写法,vue3不支持。可以更换为 v-slot
<el-table-column label="操作">
<template v-slot="obj">
<el-button type="primary" size="mini">
修改
</el-button>
<el-button type="danger" size="mini" @click="deleteRow(obj.row.id)">
删除
</el-button>
</template>
</el-table-column>
JS中,定义 deleteRow
方法,接收id参数,调用API方法,完成删除
import { getCategoryAPI, addCategoryAPI, deleteCategoryAPI } from '@/api/article.js'
其他代码略
// 删除
deleteRow(id) {
if (id === 1 || id === 2) return this.$message.error('管理员不允许删除id=1和id=2的数据')
this.$confirm('确定要删除吗?', '提示', {
callback: async r => {
// console.log(r) // cancel confirm
if (r === 'confirm') {
const { data: res } = await deleteCategoryAPI(id)
if (res.code === 0) {
this.$message.success(res.message)
this.initCategory()
} else {
this.$message.error(res.message)
}
}
}
})
},
凡是修改,必须进行数据回填。回填的数据也是来自于前面的 作用域插槽
在 ArtCate.vue
中声明修改分类的对话框:
addVisible
为 editVisible
addData
为editData
editRef
@closed="resetAddForm"
<!-- 修改分类的对话框 -->
<el-dialog title="修改类别" :visible.sync="editVisible" width="35%">
<!-- 这里放添加的表单 -->
<el-form :model="editData" :rules="addFormRules" label-width="80px" ref="editRef">
<!-- 第一项:分类名称 -->
<el-form-item label="分类名称" prop="cate_name">
<el-input v-model="editData.cate_name"></el-input>
</el-form-item>
<!-- 第二项:分类别名 -->
<el-form-item label="分类别名" prop="cate_alias">
<el-input v-model="editData.cate_alias"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editVisible = false">取 消</el-button>
<el-button type="primary">确 定</el-button>
</span>
</el-dialog>
在 data 中定义布尔值 editVisible
和数据对象 editData
:
data() {
return {
// 控制修改对话框的显示与隐藏
editVisible: false,
// 修改表单的数据对象
editData: {}
}
}
为修改按钮绑定点击事件处理函数:
<template v-slot="obj">
<el-button type="primary" size="mini" @click="editCategory(obj.row)">
修改
</el-button>
<el-button type="danger" size="mini" @click="deleteRow(obj.row.id)">
删除
</el-button>
</template>
定义 editCategory
处理函数:
// 显示修改的弹框
editCategory(row) {
if (row.id === 1 || row.id === 2) return this.$message.error('不允许修改前两条数据')
this.editVisible = true
// console.log(row)
this.editData = { ...row }
},
至此,数据回填完成。
封装修改分类的API方法
// 修改分类
export const updateCategoryAPI = data => request.put('/my/cate/info', data)
确定按钮添加单击事件
<el-button type="primary" @click="updateCategory">确 定</el-button>
组件中,完成修改方法
// 按需加载 API 方法
import {
getCategoryAPI,
addCategoryAPI,
deleteCategoryAPI,
updateCategoryAPI
} from '@/api/article.js'
其他代码略
// 修改分类
updateCategory() {
this.$refs.editRef.validate(async valid => {
if (valid) {
const { data: res } = await updateCategoryAPI(this.editData)
if (res.code === 0) {
this.$message.success(res.message)
this.initCategory()
this.editVisible = false
} else {
this.$message.error(res.message)
}
}
})
}
在 src/views/Menus/Article/
目录下,新建 ArtList.vue
组件
补充路由规则
{ path: 'art-list', component: () => import('@/views/Menu/Article/ArtList.vue') }
<template>
<div>
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>文章列表</span>
</div>
<!-- 搜索区域 -->
<div class="search-box">
<el-form :inline="true" :model="q">
<el-form-item label="文章分类">
<el-select v-model="q.cate_id" placeholder="请选择分类" size="small">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item label="发布状态" style="margin-left: 15px;">
<el-select v-model="q.state" placeholder="请选择状态" size="small">
<el-option label="已发布" value="已发布"></el-option>
<el-option label="草稿" value="草稿"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small">筛选</el-button>
<el-button type="info" size="small">重置</el-button>
</el-form-item>
</el-form>
<!-- 发表文章的按钮 -->
<el-button type="primary" size="small" class="btn-pub">发表文章</el-button>
</div>
<!-- 文章表格区域 -->
<!-- 分页区域 -->
</el-card>
</div>
</template>
<script>
export default {
name: 'ArtList',
data() {
return {
// 查询参数对象
q: {
pagenum: 1,
pagesize: 2,
cate_id: '',
state: ''
}
}
}
}
</script>
<style lang="less" scoped>
.search-box {
display: flex;
justify-content: space-between;
align-items: flex-start;
.btn-pub {
margin-top: 5px;
}
}
</style>
在 ArtList.vue
组件中,声明发表文章的对话框:
<!-- 发表文章的 Dialog 对话框 -->
<el-dialog title="发表文章" :visible.sync="pubDialogVisible" fullscreen :before-close="handleClose">
<span>这是一段信息</span>
</el-dialog>
在 data 中定义布尔值 pubDialogVisible
,用来控制对话框的显示与隐藏:
data() {
return {
// 控制发表文章对话框的显示与隐藏
pubDialogVisible: false
}
}
点击发布按钮,展示对话框:
<!-- 发表文章的按钮 -->
<el-button
type="primary"
size="small"
class="btn-pub"
@click="pubDialogVisible = true"
>
发表文章
</el-button>
在对话框将要关闭时,询问用户是否确认关闭对话框:
// 关闭事件的处理函数
handleClose(done) {
this.$confirm('关闭将会丢失已填写的数据,确定要关闭吗?', {
callback: r => {
if (r === 'confirm') {
// this.pubDialogVisible = false
done()
}
}
})
}
初步定义表单的 UI 结构:
<!-- 全屏对话框的表单 -->
<el-form :model="pubForm" :rules="pubFormRules" ref="pubFormRef" label-width="80px">
<!-- 第一项:文章标题 -->
<el-form-item label="文章标题" prop="title">
<el-input v-model="pubForm.title" placeholder="请输入标题"></el-input>
</el-form-item>
<!-- 第二项:选择分类 -->
<el-form-item label="选择分类" prop="cate_id">
<el-select v-model="pubForm.cate_id" placeholder="请选择分类" style="width: 100%">
<el-option label="北京" value="2"></el-option>
<el-option label="上海" value="5"></el-option>
</el-select>
</el-form-item>
</el-form>
在 data 中定义数据对象和验证规则对象:
data() {
return {
// 表单的数据对象
pubForm: {
title: '',
cate_id: ''
},
// 表单的验证规则对象
pubFormRules: {
title: [
{ required: true, message: '请输入文章标题', trigger: 'blur' },
{ min: 1, max: 30, message: '文章标题的长度为1-30个字符', trigger: 'blur' }
],
cate_id: [{ required: true, message: '请选择分类', trigger: 'blur' }]
}
}
}
封装获取分类的API (前面已经封装过,这里别忘记导入)
// 加载获取分类的API方法,获取分类
import { getCategoryAPI } from '@/api/article.js'
在 methods 中,声明初始化文章分类列表数据的方法:
// 初始化文章分类的列表数据
async initCateList() {
const { data: res } = await getCategoryAPI()
if (res.code === 0) {
this.cateList = res.data
} else {
this.$message.error(res.message)
}
},
在 data 中声明 cateList
数组:
data() {
return {
// 文章分类
cateList: []
}
}
在 created 生命周期函数中,调用步骤2声明的方法:
created() {
this.initCateList()
},
循环渲染文章分类的可选项:
<el-form-item label="文章分类" prop="cate_id">
<el-select v-model="pubForm.cate_id" placeholder="请选择分类" style="width: 100%;">
<!-- 循环渲染分类的可选项 -->
<el-option :label="item.cate_name" :value="item.id" v-for="item in cateList" :key="item.id">
</el-option>
</el-select>
</el-form-item>
基于 vue-quill-editor 实现富文本编辑器:https://www.npmjs.com/package/vue-quill-editor
运行如下的命令,在项目中安装富文本编辑器:
npm i vue-quill-editor@3.0.6 -S
在项目入口文件 main.js
中导入并全局注册富文本编辑器:
// 导入富文本编辑器
import VueQuillEditor from 'vue-quill-editor'
// 导入富文本编辑器的样式
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
// 全局注册富文本编辑器
Vue.use(VueQuillEditor)
在 ArtList.vue
组件的 data 中,定义富文本编辑器对应的数据项:
data() {
return {
// 表单的数据对象
pubForm: {
title: '',
cate_id: '',
// 文章的内容
+ content: ''
},
}
}
在 ArtList.vue
组件的模板结构中,添加富文本编辑器的 DOM 结构:
<el-form-item label="文章内容">
<!-- 使用 v-model 进行双向的数据绑定 -->
<quill-editor v-model="pubForm.content"></quill-editor>
</el-form-item>
美化富文本编辑器的样式:
// 设置富文本编辑器的默认最小高度
/deep/ .ql-editor {
min-height: 300px;
}
页面布局(点击按钮,触发文件域的单击事件)
<!-- 第四项:封面图片 -->
<el-form-item label="封面图片">
<div>
<img src="../../../assets/images/cover.jpg" alt="" ref="imgRef" />
</div>
<input type="file" style="display: none" ref="fileRef" @change="chooseImg" />
<el-button type="text" @click="$refs.fileRef.click()">+ 选择图片</el-button>
</el-form-item>
图片预览
// 文件域图片改变,做预览
chooseImg(e) {
if (e.target.files.length > 0) {
// 说明选择了图片
// 得到文件对象
const fileObj = e.target.files[0]
this.imgUrl = URL.createObjectURL(fileObj)
// 做好预览之后,将文件对象赋值到 添加的数据项中
this.pubForm.cover_img = fileObj
} else {
// 说明没选图片
this.pubForm.cover_img = ''
this.imgUrl = ''
}
}
把图片存储到数据项中(Ajax提交数据用)
data() {
return {
// 下面的pubForm准备一会提交到服务器的
pubForm: {
title: '',
cate_id: '',
content: '',
cover_img: ''
},
// 下面的imgUrl是做预览用的
imgUrl: ''
}
}
页面图片展示
<img :src="imgUrl" alt="" ref="imgRef" v-if="imgUrl" class="cover_img" />
<img src="../../../assets/images/cover.jpg" alt="" ref="imgRef" v-else />
样式
// 图片的样式
.cover_img {
width: 400px;
height: 280px;
// 下面的object-fit的意思是让图片不缩放,保持原比例
object-fit: cover;
}
准备数据项(添加文章的接口,需要state项)
data() {
return {
// 下面的pubForm准备一会提交到服务器的
pubForm: {
title: '',
cate_id: '',
content: '',
cover_img: '',
+ state: '', // 这个表示文章状态
}
}
}
设置两个按钮(发布和存为草稿)
<!-- 第五项:按钮 -->
<el-form-item>
<el-button type="primary">发布</el-button>
<el-button type="info">存为草稿</el-button>
</el-form-item>
点击发布和存为草稿时,修改数据项
<el-button type="primary" @click="publishArt('已发布')">发布</el-button>
<el-button type="info" @click="publishArt('草稿')">存为草稿</el-button>
// 发布文章的方法
publishArt(s) {
this.pubForm.state = s
}
发布之前,一定要用 Vue dev-tools 工具,检查一下数据项是否齐全,是否正确(是否符合接口要求)
封装API方法
// 添加文章
export const addArticleAPI = data => request.post('/my/article/add', data)
“发布” 和 “存为草稿” 的单击事件处理函数中 publishArt
:
pubForm
转为 FormData
格式(因为接口需要FormData格式)// 发布文章的方法
publishArt(s) {
// 设置文章的状态
this.pubForm.state = s
// 通过表单验证
this.$refs.pubFormRef.validate(async valid => {
if (valid) {
// 验证通过,只能说明文章标题写了,选择了分类
// 对于其他项,手动验证
if (this.pubForm.content === '') return this.$message.error('请填写内容')
if (this.pubForm.cover_img === '') return this.$message.error('请选择图片')
if (this.pubForm.state === '') return this.$message.error('请选择状态')
// console.log('验证通过')
// 接口要的是FormData格式的数据,所以需要将 pubForm对象转成FormData格式
const fd = new FormData()
for (const key in this.pubForm) {
fd.append(key, this.pubForm[key])
}
const { data: res } = await addArticleAPI(fd)
if (res.code === 0) {
this.$message.success(res.message) // 提示
this.pubDialogVisible = false // 关闭弹出层
// 更新页面数据(等做好文章列表后,在补充)
} else {
this.$message.error(res.message)
}
}
})
}
如果没有完成添加文章。可以先去演示页面去添加几条。
封装获取文章列表的API方法
// 获取文章列表数据
// 调用方法的时候,会传递请求参数过来 { pagenum: 1, pagesize: 2, cate_id: 1, state: 'xxx' }
export const getArticleAPI = obj => request.get('/my/article/list', { params: obj })
在 data 中定义如下的数据:
data() {
return {
// 文章的列表数据
artList: [],
// 总数据条数
total: 0
// ........其他略
}
}
在 methods 中声明 initArtList
函数,请求文章的列表数据:
// 获取文章列表数据
async initArtList() {
const { data: res } = await getArticleAPI(this.q)
// console.log(res)
if (res.code === 0) {
this.artList = res.data
this.total = res.total
}
}
在 created 中调用步骤 3 封装的函数:
created() {
this.initCateList()
// 请求文章的列表数据
this.initArtList()
},
在模板结构中,通过 el-table 组件渲染文章的表格数据:
<!-- 文章表格区域 -->
<el-table :data="artList" style="width: 100%;" border stripe>
<el-table-column label="文章标题" prop="title"></el-table-column>
<el-table-column label="分类" prop="cate_name"></el-table-column>
<el-table-column label="发表时间" prop="pub_date"></el-table-column>
<el-table-column label="状态" prop="state"></el-table-column>
<el-table-column label="操作"></el-table-column>
</el-table>
(回顾发布文章)在发表文章成功后,调用步骤 3 封装的 initArtList
函数
安装格式化时间的第三方包 dayjs
:
npm i dayjs -S
在项目入口文件 main.js 中导入并使用 dayjs,定义全局过滤器:
// 导入dayjs
import dayjs from 'dayjs'
// 定义全局的过滤器函数
// Vue.filter('过滤器名', fn)
Vue.filter('dtformat', dt => {
// dt 表示原本要输出的那个值。即 Wed Jan 19 2022 18:39:40 GMT+0800 (China Standard Time)
return dayjs(dt).format('YYYY-MM-DD HH:mm:ss')
})
在 ArtList.vue
组件中,调用全局过滤器,对时间进行格式化:
<el-table-column label="发表时间">
<template v-slot="{ row }">
{{ row.pub_date | dtformat }}
</template>
</el-table-column>
直接从elementUI复制过来的完整的分页
<el-pagination
@size-change="handleSizeChange" <!-- 当page-size改变的时候,触发的函数-->
@current-change="handleCurrentChange" <!-- 当前页切换的时候,比如从第3页到第4页,触发-->
:current-page="2" <!-- 当前页,比如2。页面中的第2页是蓝色的,表示正在浏览第2页数据 -->
:page-sizes="[10, 200, 300, 400]" <!-- 下拉框中,每页的条数 -->
:page-size="10" <!-- 每一页多少条数据 -->
layout="total, sizes, prev, pager, next, jumper" <!-- 自定义布局 -->
:total="400" <!-- 数据总数 -->
>
</el-pagination>
在 ArtList.vue
组件中使用 el-pagination
组件:
<!-- 分页区域 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="q.pagenum"
:page-sizes="[2, 3, 5, 10]"
:page-size="q.pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
美化样式:
.el-pagination {
margin-top: 15px;
}
声明 handleSizeChange
函数,监听 pageSize 的变化:
// pageSize 发生了变化
handleSizeChange(newSize) {
// 为 pagesize 赋值
this.q.pagesize = newSize
// 默认展示第一页数据
this.pagenum = 1
// 重新发起请求
this.initArtList()
},
声明 handleCurrentChange
函数,监听页码值的变化:
// 页码值发生了变化
handleCurrentChange(newPage) {
// 为页码值赋值
this.q.pagenum = newPage
// 重新发起请求
this.initArtList()
}
动态为筛选表单中的文章分类下拉菜单,渲染可选项列表:
<el-form-item label="文章分类">
<el-select v-model="q.cate_id" placeholder="请选择分类" size="small">
<!-- 循环渲染可选项 -->
<el-option v-for="item in cateList" :key="item.id" :label="item.cate_name" :value="item.id">
</el-option>
</el-select>
</el-form-item>
当用户点击筛选按钮时,调用 initArtList
函数重新发起数据请求:
<el-button type="primary" size="small" @click="initArtList">筛选</el-button>
当用户点击重置按钮时,调用 resetList
函数:
<el-button type="info" size="small" @click="resetList">重置</el-button>
声明 resetList
函数如下:
// 重置文章的列表数据
resetList() {
// 1. 重置查询参数对象
this.q = {
pagenum: 1,
pagesize: 2,
cate_id: '',
state: ''
}
// 2. 重新发起请求
this.initArtList()
}
运行 npm run build
即可生成 dist
文件夹。这就是打包之后的结果。
项目根目录创建 Vue 的配置文件 vue.config.js
module.exports = {
// 移除掉打包后生成的 xxx.map 文件
productionSourceMap: false,
}
module.exports = {
// 配置publicPath 为 '' 或者 './' 则打包后的结果,可以通过 File 协议直接预览。
publicPath: '',
// 移除掉打包后生成的 xxx.map 文件
productionSourceMap: false,
}
打开 package.json
配置文件,为 scripts
节点下的 build
命令添加 --report
参数:
{
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --report",
"lint": "vue-cli-service lint"
}
}
重新运行打包的命令:
npm run build
打包完成后,发现在 dist
目录下多了一个名为 report.html
的文件。在浏览器中打开此文件,会看到详细的打包报告。
externals
之前:
import
导入的第三方模块,在最终打包完成后,会被合并到 chunk-vendors.js
中externals
之后:
externals
节点下声明的第三方包排除在外externals
节点下的包我们使用的CDN加速,只是减少了打包的体积,并不一定会加快网站的打开速度。
在项目根目录下创建 vue.config.js
配置文件,在里面新增 configureWebpack
节点如下:
module.exports = {
// 省略其它代码...
// 增强 vue-cli 的 webpack 配置项
configureWebpack: {
// 打包优化
externals: {
// import导包的包名: window全局的成员名称
echarts: 'echarts',
}
}
}
在 /public/index.html
文件的 <body>
结束标签之前,新增如下的资源引用:
<!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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
</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://cdn.jsdelivr.net/npm/echarts@5.2.1/dist/echarts.min.js"></script>
</body>
</html>
重新运行 npm run build
命令,对比配置 externals
前后文件的体积变化。
在 vue.config.js
配置文件中,找到 configureWebpack
下的 externals
,添加如下的配置项:
module.exports = {
// 省略其它代码...
publicPath: './',
// 增强 vue-cli 的 webpack 配置项
configureWebpack: {
// 打包优化
externals: {
// import导包的包名: window全局的成员名称
echarts: 'echarts',
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
axios: 'axios',
dayjs: 'dayjs',
'element-ui': 'ELEMENT',
'vue-quill-editor': 'VueQuillEditor',
'vuex-persistedstate': 'createPersistedState'
}
}
}
在 /public/index.html
文件的 <body>
结束标签之前,添加如下的 js 引用:
<!-- built files will be auto injected -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.2.1/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@3.5.2/dist/vue-router.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.21.3/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.10.6/dayjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.6/lib/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-quill-editor@3.0.6/dist/vue-quill-editor.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuex-persistedstate@3.2.1/dist/vuex-persistedstate.umd.js"></script>
在 main.js
中注释掉 element-ui
的样式和 quill
的样式:
// 1. 导入 element-ui 组件库的样式
// import 'element-ui/lib/theme-chalk/index.css'
// 2. 导入 quill 的样式
// import 'quill/dist/quill.core.css'
// import 'quill/dist/quill.snow.css'
// import 'quill/dist/quill.bubble.css'
在 /public/index.html
文件的 <title></title>
标签之后,引入需要的 css 样式:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.15.6/lib/theme-chalk/index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.core.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.bubble.css">
参考 vue-router 的官方文档,进行路由懒加载的配置
配置路由懒加载的核心步骤:
运行如下的命令,安装 babel 插件:
npm install --save-dev @babel/plugin-syntax-dynamic-import
修改项目根目录下的 babel.config.js
配置文件,新增 plugins
节点:
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
// 实现路由组件按需导入的 babel 插件
+ plugins: ['@babel/plugin-syntax-dynamic-import']
}
在 /src/router/index.js
模块中,基于 const 组件 = () => import('组件的存放路径')
语法,这个我们一开始就这样做的,无需修改。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。