在《准备篇》中,我们已经搭建好了Vue的开发和部署环境,并且尝试使用vue-cli脚手架构建了第一个Vue项目testvue。下面我们就在testvue的基础上进行完善,希望它能具备以下特性和功能:
1)整个页面布局由顶栏、侧边栏、主体区域组成。顶栏展示网站名称,当前用户昵称。昵称上鼠标悬停展示个人中心、退出登录等按钮。侧边栏显示当前用户可操作的菜单,刚登录进来右侧主体区域显示首页欢迎语,点击菜单后主体区域显示当前菜单内容。主体区域的第一行显示面包屑菜单。
2)用户未登录或登录已过期时,点击任何操作都跳转到登录页。登录成功将当前用户token,uuid保存到cookie,退出登录时清除上述cookie。
3)不同用户可操作的菜单不同,登录成功后调后端接口获取当前用户菜单。
4)订单页展示订单列表,点击订单号跳转到该订单详情页,列表页具有查询和重置功能。
5)由于本文只介绍Vue的相关知识,所有后端接口都采用node.js 来mock数据,可参考:https://www.jianshu.com/p/3becdd6ea766。 接口的数据结构统一采用如下结构,result中包含返回的数据信息:
{
"errorCode":0,
"errorMessage":"成功",
"result":{ xxxxxx }
}
效果图如下:
本例中技术栈采用:Vue.js + vuex + vue-router + axios + element-ui
常用参考文档:
1) Vue.js:https://cn.vuejs.org/v2/api/
2) Vuex :https://vuex.vuejs.org/zh/guide/state.html
3) vue-router:https://router.vuejs.org/zh/
4) vue-axios:http://www.axios-js.com/zh-cn/docs/vue-axios.html
5) ES6:https://www.runoob.com/w3cnote/es6-tutorial.html
6) element-ui:https://element.eleme.cn/
一、 入口文件介绍:App.vue及main.js
- App.vue内容如下:
<template>
<div id="app">
<router-view/>
</div>
</template>
- main.js内容如下:
//注释1:引入相关插件、组件等
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import util from '@/libs/util'
//注释2:引入element-ui
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
Vue.config.productionTip = false
//注释3:创建Vue根实例
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
//注释4:路由拦截,权限验证
router.beforeEach((to, from, next) => {
// 验证当前路由所有的匹配中是否需要有登录验证的
if (to.matched.some(r => r.meta.auth)) {
// 这里暂时将cookie里是否存有token作为验证是否登录的条件
const token = util.cookies.get('token');
if (token && token !== 'undefined') {
next()
} else {
// 没有登录的时候跳转到登录页
location.href = "/"
}
} else {
// 不需要身份校验 直接通过
next()
}
})
router.afterEach(to => {
})
App.vue是Vue应用程序的根实例,也是其核心所在。main.js作为入口js,与App.vue配合完成根实例的构建。
main.js中有几点需要解释:
1)注释1处通过import引入了App.vue,由于Vue中引入文件、组件、插件等都可以省略后缀,import App from ‘./App’ 即引入当前目录下的App.vue 。另外需要注意的是import util from ‘@/libs/util’ 的路径中存在@符号,表示根目录,即src。因此“.”和“@”的写法可理为相对路径和绝对路径。
2)注释2处引入了element-ui的js和css,并通过Vue.use(ElementUI)调用。小伙伴可能存在疑问的地方是:为什么有的插件import了就可以直接用,而有的插件需要Vue.use()后才能使用呢?Vue.use()使用后调用的是组件的install 方法,所以引入前插件如果含install方就需要调用Vue.use(),否则可直接使用。
3)注释3处通过调用Vue的构造函数Vue()创建根实例,向该构造函数传入参数,提供构造应用程序所需要的的选项。其中选项el用来指定一个DOM元素,本例中即指定了App.vue中id为app的元素,vue会在这个元素上挂载当前应用程序。后面传入router和store, 用于在Vue实例中暴露这些属性或者方法,后续可直接在各组件中使用,使用的时候需增加$前缀,用于和用户定义属性区分开来,如this.$router。
Vue对象参数的几个常用选项包括:挂载元素el、data函数成员、methods对象成员、模板template、生命周期钩子、props属性声明、computed计算成员、watch监视成员。此处只简要介绍,后面遇到时再详细说明。另外,一个组件通常需要导出为一个对象,供其他组件中通过import引用,其步骤通常如下:
var app= new Vue({
xxxxxxxxxxxxxxxx
})
//默认输出,可在其他组件引用
export default app
也可以直接通过如下方法导出vue对象,下述方法貌似被使用的更广泛:
export default {
name: '',
components: {},
data: () {}, // data函数成员
watch: {}, // watch监视成员
computed: {}, // computed计算成员
created: function () {},
methods: {}, // methods对象成员
actions: {}
}
由于main.js中的根组件对应的Vue实例不需要给其他组件使用,因此不需要export。
4)注释4为路由拦截(路由的内容在第三部分介绍,小伙伴可以后面返回再来读这部分)。路由切换时都会执行这段代码。router.beforeEach是指在路由跳转之前执行。三个参数的含义如下:
a)to: router即将进入的路由
b)from: 当前导航即将离开的路由
c)next: 是一个方法,进行管道中的一个钩子,如果执行完了,则导航的状态就是 confirmed (已确认);否则为false,即终止导航。
需要解释的一句代码:if (to.matched.some(r => r.meta.auth)),to.matched表示即将进入的路由匹配上的所有路由记录,是一个数组。.some() 方法即测试该数组中的某些元素是否通过了指定函数的测试,这个指定函数即括号中的r => r.meta.auth,箭头函数,等价于:
function(r){
return r.meta.auth;
}
举个例子,如果to(目的路由)为:
{
path: '/user',
name: User,
meta: {
title: '测试',
auth: true
}
则if (to.matched.some(r => r.meta.auth))执行记结果即为true。总结来说,路径中的meta的auth属性为true的,都需要通过路由拦截,进行后续一系列关于token的判断,为false则直接通过。
二、整体布局:
下面我们开始编写布局文件,包含顶栏、侧边栏、主体区域。首页仅显示简单的欢迎。配置/home路径与该布局文件绑定。
- 在src目录下新建layouts文件夹,新建layout.vue布局文件。内容如下:
<template>
<div>
<el-container>
<!--顶栏-->
<el-header class="homeHeader">
<div class="title">后台管理系统</div>
<div>
<!-- 注释1: @ command -->
<el-dropdown class="userInfo" @command="commandHandler">
<!--注释2: {{}} -->
<span class="el-dropdown-link">{{currentUser.name}}</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="userinfo">个人中心</el-dropdown-item>
<el-dropdown-item command="setting">设置</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-header>
<el-container>
<!--侧边栏-->
<el-aside width="200px">
<el-menu router unique-opened>
<!--注释3::index -->
<el-submenu :index="index+''" v-for="(item,index) in routes" :key="index">
<template slot="title">
<i style="color: #409eff;margin-right: 5px" :class="item.iconCls"></i>
<span>{{item.name}}</span>
</template>
<!--注释4:v-for -->
<el-menu-item
:index="child.path"
v-for="(child,indexj) in item.children"
:key="indexj"
>{{child.name}}</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<!--主体部分-->
<el-main>
<!--面包屑菜单 注释5.:v-if -->
<el-breadcrumb
separator-class="el-icon-arrow-right" v-if="this.$router.currentRoute.path!='/home'">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{this.$router.currentRoute.name}}</el-breadcrumb-item>
</el-breadcrumb>
<div class="homeWelcome" v-if="this.$router.currentRoute.path=='/home'">欢迎您!</div>
<!--路由容器-->
<router-view class="homeRouterView" />
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
//注释6:mapState
import { mapState } from "vuex";
import util from "@/libs/util.js";
export default {
name: "Home",
//注释7:计算属性
//使用vuex的辅助方法:mapState代替this.$store.state.routes的复杂写法
//computed: mapState(["routes", "currentUser"]),
computed: {
...mapState(["routes"]),
currentUser() {
return this.$store.state.currentUser;
}
},
methods: {
commandHandler(cmd) {
if (cmd == "logout") {
this.$confirm("此操作将注销登录, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
// 删除cookie
util.cookies.remove("token");
util.cookies.remove("uuid");
// 清空 store 用户和菜单路由信息
this.$store.commit("initUser", {});
this.$store.commit("initRoutes", null);
// 跳转路由
location.href = "/";
})
.catch(() => {
this.$message({
type: "info",
message: "已取消操作"
});
});
} else if (cmd == "userinfo") {
this.$router.push("/user/info");
}
}
}
};
</script>
<style>
.homeRouterView {
margin-top: 10px;
}
.homeWelcome {
text-align: center;
font-size: 30px;
font-family: 华文行楷;
color: #409eff;
padding-top: 50px;
}
.homeHeader {
background-color: #409eff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px 15px;
box-sizing: border-box;
}
.homeHeader .title {
font-size: 30px;
font-family: 华文行楷;
color: #ffffff;
}
.homeHeader .userInfo {
cursor: pointer;
}
.el-dropdown-link {
display: flex;
align-items: center;
}
</style>
layout.vue是一个单文件组件(组件的知识后面再详细介绍),和App.vue一样,每个.vue文件都是一个Vue实例,包含三种类型的顶级语言块 <template>, <script> 和 <style>,这三个部分分别代表了 html、js、css。
由于main.js中引入了element-ui,此处可以直接使用。Layout.vue中标签包裹的内容就是使用element-ui编写的html,几个重要组件已在代码上方注释。element-ui有非常丰富的组件库,详情可查看官方文档,例如布局部分的文档如下:
需要注意的是主体部分中的,router-view可以理解为一个容器,它渲染的组件是我们使用 vue-router 指定的,因此就可以方便的实现点击左侧菜单切换路由时,其他部分不变,只右侧主体部分(router-view标签所在位置)内容随之切换的效果。
另外有几处需要稍加解释:
1)注释1的 @ command:由element-ui文档可知,command为点击Dropdown下拉菜单项事件。并且由Vue.js官方文档可知,@符号则是v-on指令的缩写,用于监听事件,文档如下:
因此@command=“commandHandler” 即点击菜单项时触发commandHandler()函数。同理@click为点击事件,@select为选择事件等等。@后的事件名称可查找element-ui文档获得,如table组件的事件如下图所示:
2)注释2的{{}} :这种双花括号的写法是Vue借用了Mustache中的{{…}}语法来进行文本插值,但并不支持完整的Mustache定义。本例中是将currentUser的name属性显示在此: {{currentUser.name}}。
3)注释3处的:index:动态绑定index属性,是v-bind:index的缩写。
同理<img v-bind:src=“imageSrc”>可缩写为:<img :src=“imageSrc”>。也就是当src的内容为变量时使用:src=“imageSrc”,src内容不变时则直接src=“xxx.png “即可,不用加冒号。
4)注释4处的v-for=”(child,index) in item.children”:即遍历item的children属性内容,其中child表示当前元素,index为当前元素的索引。
5)注释5处的v-if:根据v-if后的表达式有条件的渲染当前元素。
至此,Layout.vue的html部分基本介绍完,接下来介绍下script部分中几个比较重要的知识点。
1)注释6处的import { mapState } from “vuex” 即引入vuex的辅助工具mapState。要使用mapState,首先要了解vuex是什么。从官方文档:https://vuex.vuejs.org/zh/ 可知,vuex是专门为Vue开发的状态管理模式,集中的存储整个应用程序的状态,一般应用在多个组件需要共享数据或状态的场景。本例中,我们希望当前用户的账号信息和菜单数据可以统一管理,然后在各个组价之间共享。实现方式是,在src目录下创建store目录,新建index.js文件,内容如下:
import Vue from 'vue'
import Vuex from 'vuex'
//引入api目录下封装的请求远程接口的方法
import { getData } from "@/api/request";
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
routes: '',
currentUser: ''
},
mutations: {
initUser(state, user) {
state.currentUser = user;
},
initRoutes(state, userId) {
if (userId == null) {
state.routes = [];
} else {
//根据userId获取菜单列表
getData("/api/menus", userId).then(data => {
state.routes = data;
});
}
},
},
actions: {
}
})
export default store;
Vuex有几个常用的概念:State、Getter、Mutation、Action。
- State:存储状态数据。
- Mutation:存储用于同步更改状态数据的方法,默认传入的参数为state。
- Action:存储用于异步更改状态数据,但不是直接更改,而是通过触发Mutation方法实现。
由于main.js中已近通过new Vue()的参数将store实例注入到了所有子组件中:
new Vue({
el: '#app',
router,
//把 store 的实例注入所有的子组件
store,
components: { App },
template: '<App/>'
})
调用方法如下:
- state中状态的获取:this.$store.state.routes;
- mutation中同步方法的调用:this.$store.commit(‘xxx’);
- actions中异步方法的调用:this.$store.dispatch(‘xxx’);
可以看出来上述写法比较繁琐,这也就是我们上文中提到的mapState要解决的问题。
import { mapState } from "vuex";
computed: mapState(["routes", "currentUser"])
之后就可以直接使用routes和currentUser了,与如下写法有相同的效果:
computed: {
routes() {
return this.$store.state.routes;
},
currentUser() {
return this.$store.state.currentUser;
}
}
与mapState 类似的还有mapMutations,、mapActions,分别用来完成mutations和actions中方法的辅助调用。
小伙伴可能还有疑问的一点就是computed,前文中提到过computed是Vue的计算属性。当其依赖的属性的值发生变化时,计算属性会重新计算,反之则使用缓存中的属性值。不像methods里的方法,每次遇到都执行。
此外,Layout.vue中<style>标签包裹的是样式,都比较简单,此处不赘述。
三、路由配置:
我们在第二部分中编写了一个布局文件,包含顶栏侧边栏和主体等内容,那么如何将这个页面与浏览器中的请求路径联系起来呢?这就需要借助于路由,我们使用vue的官方路由管理器vue-router来管理路由。当URL发生变动时,Vue路由器会拦截请求请显示相应的路由。
为了方便演示,我们在src目录下新建views文件夹,创建多个vue文件表示不同组件,内容可以只写简单的页面名称用以区分,如Auth.vue内容如下,其他几个文件内容都类似:
然后在src的router目录下的index.js文件中引入上述组件,并配置路由:
import Vue from 'vue'
import Router from 'vue-router'
import Layout from '@/layouts/layout'
import Auth from '@/views/Auth'
import Account from '@/views/Account'
import Order from '@/views/Order'
import Refund from '@/views/Refund'
import Login from '@/views/Login'
import Userinfo from '@/views/Userinfo'
Vue.use(Router)
export default new Router({
//设置路由的 history 模式,解决路径中#问题
mode: 'history',
routes: [
{
path: '/',
name: 'Login',
component: Login
},
{
path: '/home',
name: 'Layout',
component: Layout,
meta: { auth: true }
},
{
path: '/user',
name: '用户管理',
component: Layout,
meta: { auth: true },
children: [
{
path: '/user/auth',
name: '权限设置',
component: Auth,
meta: { auth: true }
}, {
path: '/user/account',
name: '账号管理',
component: Account,
meta: { auth: true }
},{
path: '/user/info',
name: '个人中心',
component: Userinfo,
meta: { auth: true }
}]
}, {
path: '/order',
name: '订单管理',
component: Layout,
meta: { auth: true },
children: [
{
path: '/order/myOrder',
name: '我的订单',
component: Order,
meta: { auth: true }
}, {
path: '/order/myRefund',
name: '我的退换货',
component: Refund,
meta: { auth: true }
}]
},
{
path: '*',
redirect: '/home'
}
]
})
特别需要注意的是:第一部分main.js中介绍过路由拦截router.beforeEach()会对所有meta: { auth: true } 的路由进行拦截,未登录则跳转到根路由/,因此根路由“/”一定不要添加meta: { auth: true },否则会形成死循环,无法正常跳转。
最后path: '*'的配置,是指之前的所有路由都没有匹配上时,会被该配置拦截,也就是路径不存在时跳转到home页,此处也可以配置跳转到错误页。需要注意的一点是,该配置需放在最后,不然所有路由都会被它拦截。
这样,路由和组件之间的对应关系已经有了,下面就是编写登录页,登陆成功后调接口获取当前用户所有有权限的路由,并把路由列表渲染在左侧菜单中。
在src目录下创建Login.vue文件,内容如下:
<template>
<div>
<el-form
:rules="rules"
ref="loginForm"
v-loading="loading"
element-loading-text="正在登录..."
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
:model="loginForm"
class="loginContainer"
>
<h3 class="loginTitle">登录</h3>
<el-form-item prop="username">
<el-input
size="normal"
type="text"
v-model="loginForm.username"
auto-complete="off"
placeholder="请输入用户名"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
size="normal"
type="password"
v-model="loginForm.password"
auto-complete="off"
placeholder="请输入密码"
></el-input>
</el-form-item>
<el-button size="normal" type="primary" style="width: 100%;" @click="submitLogin">登录</el-button>
</el-form>
</div>
</template>
<script>
import { postData } from "@/api/request";
import { mapMutations } from "vuex";
import util from "@/libs/util.js";
export default {
name: "Login",
data() {
return {
loading: false,
loginForm: {
username: "test",
password: "test123"
},
rules: {
username: [
{ required: true, message: "请输入用户名", trigger: "blur" }
],
password: [{ required: true, message: "请输入密码", trigger: "blur" }]
}
};
},
methods: {
...mapMutations(["initUser", "initRoutes"]),
submitLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true;
postData("/api/login", this.loginForm).then(
resp => {
this.loading = false;
if (resp) {
//将登录后返回的用户信息存储到state中
//this.$store.commit("initUser", resp);
//使用vuex的辅助方法mapMutations代替:this.$store.commit("initUser", resp)的复杂写法
this.initUser(resp.userInfo);
//将uuid和token存入cookie
util.cookies.set("uuid", resp.uuid);
util.cookies.set("token", resp.token);
//获取该用户权限菜单并初始化路由
this.$store.commit("initRoutes", resp.userInfo.id);
//跳转到home页
let path = this.$route.query.redirect;
this.$router.replace(path == "/" || path == undefined ? "/home" : path);
} else {
alert("登陆失败");
}
}
);
} else {
return false;
}
});
}
}
};
</script>
<style>
.loginContainer {
border-radius: 15px;
background-clip: padding-box;
margin: 120px auto;
width: 350px;
padding: 15px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.loginTitle {
margin: 15px auto 20px auto;
text-align: center;
color: #505458;
}
.loginRemember {
text-align: left;
margin: 0px 0px 15px 0px;
}
.el-form-item__content {
display: flex;
align-items: center;
}
</style>
调接口获取到的菜单数据是router中路由数据的子集,如后端返回如下数据,则左侧菜单只展示“权限设置”和“我的订单”2个菜单:
{
path: '/user',
name: '用户管理',
children: [
{
path: '/user/auth',
name: '权限设置',
}
]
}, {
path: '/order',
name: '订单管理',
children: [
{
path: '/order/myOrder',
name: '我的订单',
}
]
}
需要注意的一点是调用接口的方法。Vue官方推荐使用axios请求后台资源。我们上文中提到的getData,postDate等方法都是对axios接口的封装,分别用来发送get请求和post请求。本例中我们在src目录下创建plugins/axios文件夹,在该文件夹下创建index.js文件,其内容如下:包含创建axios实例,请求和响应的拦截。
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import util from '@/libs/util'
// 创建一个错误
function errorCreate(msg) {
const error = new Error(msg)
errorLog(error)
throw error
}
// 记录和显示错误
function errorLog(error) {
// 打印到控制台
if (process.env.NODE_ENV === 'development') {
console.log(error)
}
// 显示提示
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
}
// 创建一个 axios 实例
const service = axios.create({
baseURL: process.env.VUE_APP_API,
timeout: 5000 * 3 // 请求超时时间
})
// 请求拦截器
service.interceptors.request.use(
config => {
return config
},
error => {
// 发送失败
console.log(error)
Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
// dataAxios 是 axios 返回数据中的 data
const dataAxios = response.data
// 这个状态码是和后端约定的
const { errorCode } = dataAxios
// 根据 code 进行判断
if (errorCode === undefined) {
// 如果没有 code 代表这不是项目后端开发的接口
return dataAxios
} else {
// 有 code 代表这是一个后端接口 可以进行进一步的判断
switch (errorCode) {
case 0:
// [ 示例 ] code === 0 代表没有错误
return dataAxios.result
case -1:
// [ 示例 ] 其它和后台约定的 code
errorCreate(`[ code: -1 ] ${dataAxios.errorMessage}: ${response.config.url}`)
break
case 302:
MessageBox({
title: '跳转提示',
message: `${dataAxios.errorMessage}`,
confirmButtonText: '登录',
callback: () => {
location.href = '/'
}
})
break
default:
// 不是正确的 code
util.message.error(`${dataAxios.errorMessage}`)
break
}
}
},
error => {
if (error && error.response) {
switch (error.response.status) {
case 400: error.message = '请求错误'; break
case 401: {
MessageBox({
title: '跳转提示',
message: '未授权,请登录',
confirmButtonText: '登录',
callback: () => {
location.href = '/'
}
})
break
}
case 403: error.message = '拒绝访问'; break
case 404: error.message = `请求地址出错: ${error.response.config.url}`; break
case 408: error.message = '请求超时'; break
case 500: error.message = '服务器内部错误'; break
case 501: error.message = '服务未实现'; break
case 502: error.message = '网关错误'; break
case 503: error.message = '服务不可用'; break
case 504: error.message = '网关超时'; break
case 505: error.message = 'HTTP版本不受支持'; break
default: break
}
}
errorLog(error)
return Promise.reject(error)
}
)
export default service
其中响应拦截器主要针对后端返回的错误码来决定后续处理,该错误码只要和当前项目的后端开发约定好即可,没有统一的规定。如本例中约定返回302表示未授权,需跳转到登录页。下面进行axios中各种类型请求的封装:get、post、put、delete等。在src目录下创建api目录,该目录下创建request.js文件,内容如下:
import request from '@/plugins/axios'
// 获取数据
export function getData(url, params) {
return request({
url,
method: 'get',
params
})
}
// 提交数据
export function postData(url, data) {
return request({
url,
method: 'post',
data
})
}
// 提交数据
export function postFormData(url, data, headers) {
return request({
url,
method: 'post',
data,
headers
})
}
// 更新数据
export function putData(url, data) {
return request({
url,
method: 'put',
data
})
}
// 删除数据
export function deleteData(url, data) {
return request({
url,
method: 'delete',
data
})
}
使用时按需引入,如只引入getDate和postData:
import { getData, postData } from "@/api/request";
至此,基本的框架已经搭建起来,具备了登录,动态菜单渲染,路由切换等功能。也引入了element-ui,vue-router,vuex,axios等,辅助我们编写页面及实现一些特定的功能。
四、高级组件:
在Vue.js中,组件是一个非常强大的概念,它可以减少或简化代码。运用组件,可以提取出代码的重复部分,并在整个应用程序中复用。本例中我们采用组件的方式编写订单页面。在views目录下创建Order文件夹,下面包含index.vue文件和conponents文件夹,conponents文件夹下是各组件所在的文件夹,如本例中的SearchBlock和SearchTableBlock,分别为订单页面的顶部搜索部分和表格部分,如下图红框所示:
- 在SearchBlock文件夹下创建index.vue,内容如下:
<template>
<div class="search-block">
<el-form ref="form" :model="form" size="mini" label-position='left' label-width="80px">
<el-row :gutter="30">
<el-col :span="6">
<el-form-item label="订单ID">
<!--注释1:v-model -->
<el-input v-model="form.orderId" width="100%" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="订单状态">
<el-select v-model="form.orderStatus" placeholder="不限" width="100%" clearable style="width: 100%;">
<el-option
v-for="item in orderStatusOptions"
:key="item.value"
:label="item.name"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="筛选时间">
<el-date-picker
width="100%"
v-model="form.date"
type="daterange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
></el-date-picker>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script>
import { getData } from "@/api/request";
export default {
name: "SearchBlock",
//注释2:props父子组件间传递数据
props: {
// 筛选条件
form: Object
},
//注释3:created生命周期钩子
created() {
//获取订单状态列表
this.getOrderStatusOptions();
},
data() {
return {
orderStatusOptions: []
};
},
methods: {
getOrderStatusOptions() {
getData(`/api/order-status`).then(data => {
this.orderStatusOptions = data || [];
});
}
}
};
</script>
<style scoped>
.search-block {
margin-top: 20px;
}
</style>
- 在SearchTableBlock文件夹下创建index.vue,内容如下:
<template>
<!--表格展示-->
<div>
<!-- 操作 -->
<div class="table-action">
<div>
<el-button size="mini" type="primary" @click="handleSearch()">查询</el-button>
<!-- 注释4:$emit 向父组件传递数据 -->
<el-button size="mini" type="primary" @click="$emit('reset')">重置</el-button>
</div>
</div>
<el-table
:data="data"
border
style="width: 100%"
size="mini"
v-loading="loading"
header-align="center"
>
<el-table-column prop="orderId" label="订单号">
<template slot-scope="scope">
<router-link type="text" :to="`/order/detail/${scope.row.orderId}`">{{scope.row.orderId}}</router-link>
</template>
</el-table-column>
<el-table-column prop="orderStatus" label="订单状态"></el-table-column>
<el-table-column prop="receiver" label="收件人"></el-table-column>
<el-table-column label="编辑">
<!-- 注释5:slot-scope -->
<template slot-scope="scope">
<div class="action_cell">
<el-button
round
type="primary"
size="mini"
slot="reference"
@click="handleEdit(scope.row)"
:disabled="!scope.row.canEdit"
>编辑</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
class="table-pagination"
layout="total, prev, pager, next"
@current-change="handleChange"
:current-page="currentPage"
:page-size="size"
:total="total"
></el-pagination>
</div>
</template>
<script>
export default {
name: "SearchTableBlock",
components: {},
props: {
data: {
type: Array,
default: []
},
page: {
type: Number,
default: 1
},
total: {
type: Number,
default: 0
},
loading: {
type: Boolean,
default: true
}
},
data() {
return {
// 每页几条
size: 15,
// 当前页
currentPage: 1
};
},
methods: {
//查询
handleSearch() {
//子组件向父组件传值
this.$emit("search", 1);
},
handleChange() {}
}
};
</script>
<style scoped>
.table-action {
display: flex;
flex-direction: row;
justify-content: end;
padding: 9px 14px;
border: 1px solid #EBEEF5;
border-bottom: 0px;
}
.table-pagination.el-pagination {
text-align: right;
margin-top: 20px;
}
</style>
- Order文件夹下的index.vue是上述2组件的父组件,内容如下:
<template>
<div>
<!--条件筛选组件-->
<SearchBlock :form="form" />
<!-- 表格组件 -->
<SearchTableBlock
:data="tableData"
:page="pageNo"
:total="total"
:loading="loading"
@search="handleSearch"
@reset="handleReset"
/>
</div>
</template>
<script>
import { getData } from "@/api/request";
import cloneDeep from "lodash/cloneDeep";
import SearchBlock from "./components/SearchBlock";
import SearchTableBlock from "./components/SearchTableBlock";
// 筛选条件 字段
const formData = {
orderId: "",
orderStatus: "",
date: []
};
export default {
name: "Order",
components: {
SearchBlock,
SearchTableBlock
},
data() {
return {
form: {
...formData
},
// 当前页
pageNo: 1,
// 总数
total: 0,
// table数据
tableData: [],
// loading
loading: false
};
},
created() {
this.handleSearch(1);
},
methods: {
// 查询接口
handleSearch(page) {
this.loading = true;
let startDate, endDate;
if (this.form.date != null) {
startDate = this.form.date[0];
endDate = this.form.date[1];
}
//拼接请求参数
let param = {
orderId: this.form.orderId,
orderStatus: this.form.orderStatus,
startDate: startDate,
endDate: endDate,
pageNum: page,
pageSize: 15
};
// 获取table数据
getData("/api/order-list", param).then(data => {
this.tableData = data.resultList;
this.total = data.totalNum;
this.loading = false;
});
},
// 重置
handleReset() {
this.form = cloneDeep(formData);
}
}
};
</script>
需要注意的几点如下:
1)注释1:SearchBlock中的v-model:在表单中使用,实现数据的双向绑定。
<el-input v-model=“form.orderId” width=“100%” clearable>
所谓“双向”,大概可以理解为:数据的变化会反映到组件上,组件的输入改变时也会反映到其绑定的数据上,这也就是其与v-bind(缩写是“:”)的不同之处。
2)注释2:使用props实现父子组件间传递数据。在父组件中使用SearchBolck时:
<SearchBlock :form=“form” /> 传入了form,即SearchBlock中的props里定义的内容:
props: {
form: Object
}
3)注释3:created生命周期钩子。从官方文档可知,Vue的生命周期如下图,当执行到不同的阶段,会触发相应阶段的钩子函数。本例中的created,会在实例创建完成后被调用,即实例创建完成后调用“获取订单状态列表”的方法,初始化订单状态下拉框的选项数据。
4)SearchTableBlock中的注释4:$emit(‘reset’),即向父组件的reset传递数据,此处数据为空,不为空时可写成:$emit(‘reset’,data),将data数据传递给父组件的reset事件函数。如本例中,父组件使用SearchTableBlock时:即SearchTableBlock中点击重置按钮时,会调用其父组件的handleReset方法。
<SearchTableBlock
:data="tableData"
:page="pageNo"
:total="total"
:loading="loading"
@search="handleSearch"
@reset="handleReset"
/>
5)SearchTableBlock中的注释5:slot-scope,为作用域插槽。它就像一个临时变量,包含从组件传入的属性,如本例中,使用slot-scope获取当前行的数据。除了这里介绍的作用于插槽外,插槽的种类还有很多,用法也不尽相同,小伙伴们可以自行查阅资料了解更多内容。
<template slot-scope="scope">
<div class="action_cell">
<el-button
round
type="primary"
size="mini"
@click="handleEdit(scope.row)"
:disabled="!scope.row.canEdit"
>编辑</el-button>
</div>
</template>
四、其他:
除了上述几部分比较大的知识模块外,还有些零散的小功能,下面我们介绍下其实现方法。
1.动态路由:
第三部分中已经完成了订单列表页,我们希望点击订单号时跳转到该订单的详情页。该功能可通过匹配带参数的动态路由来实现,如订单号为:234567890,则详情页的路径为:/order/detail/234567890。在router文件中,动态路由的配置是通过冒号(:)来实现的,如下:
{
path: '/order/detail/:id',
component: OrderDetail,
meta: { title: '订单详情' }
}
详情页Orderdetail.vue内容如下:
<template>
<div>
订单详情:{{this.$route.params.id}}
</div>
</template>
<script>
export default {
name: "Orderdetail"
}
</script>
this.$route.params.id获取的即路径中的参数,即orderId。
订单列表页中的跳转是通过如下方式实现的:
<el-table-column prop="orderId" label="订单号">
<template slot-scope="scope">
<router-link type="text" :to="`/order/detail/${scope.row.orderId}`">{{scope.row.orderId}}</router-link>
</template>
</el-table-column>
2.退出登录:
在顶栏的右侧点击用户名可看到包含“退出登录”的下拉菜单。退出登录的实现主要是清除store中的全局用户状态数据,清除cookie中的token和uuid,然后跳转到登录页。内容如下(上文中layout.vue中也有这段代码):
commandHandler(cmd) {
//退出登录
if (cmd == "logout") {
this.$confirm("此操作将注销登录, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
// 删除cookie
util.cookies.remove("token");
util.cookies.remove("uuid");
// 清空 store 用户和菜单路由信息
this.$store.commit("initUser", {});
this.$store.commit("initRoutes", null);
// 跳转路由
location.href = "/";
})
.catch(() => {
this.$message({
type: "info",
message: "已取消操作"
});
});
} else if (cmd == "userinfo") {
this.$router.push("/user/info");
}
}
3.watch的用法:
watch是Vue中的一个重要概念,我们在第一部分介绍Vue构造函数的参数时有提及,用来监测Vue实例中的数据变动。watch的一个简单应用如下:
1)订单列表页的编辑按钮改成组件的形式:
<el-table-column label="编辑">
<template slot-scope="scope">
<div class="action_cell">
<edit-dialog
:row="scope.row"
:canEdit="scope.row.canEdit"
@updateOrder="updateOrder"
>
</div>
</template>
</el-table-column>
2)编辑组件的内容如下:
<template>
<div :style="style.editDialog">
<el-button
round
@click="handleColumnClick()"
type="primary"
size="mini"
:disabled="!canEdit"
>编辑</el-button>
<el-dialog title="编辑" :visible.sync="dialogFormVisible">
<el-form :model="formRow" :rules="rules" ref="useMatrixForm">
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="订单号" :label-width="formLabelWidth" prop="orderId">
<el-input v-model="formRow.orderId"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="订单状态" :label-width="formLabelWidth" prop="orderStatus">
<el-select v-model="formRow.orderStatus" placeholder="请选择">
<el-option
v-for="item in orderStatusOptions"
:key="item.value"
:label="item.name"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="收件人" :label-width="formLabelWidth" prop="receiver">
<el-input v-model="formRow.receiver"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="handleSubmit('useMatrixForm')">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getData, postFormData } from "@/api/request";
import util from "@/libs/util";
export default {
//注释1:监听dialogFormVisible数据变化
watch: {
dialogFormVisible: function(val) {
if (val) {
this.formRow = {
...this.row
};
}
}
},
created() {
//获取订单状态列表
this.getOrderStatusOptions();
},
data() {
return {
style: {
editDialog: {
display: "inline-block",
marginRight: "5px"
}
},
dialogFormVisible: false,
formRow: {},
//订单状态选项
orderStatusOptions: [],
rules: {
orderId: [{ required: true, message: "订单号必填", trigger: "blur" }],
orderStatus: [
{ required: true, message: "订单状态必填", trigger: "blur" }
],
receiver: [{ required: true, message: "收件人必填", trigger: "blur" }]
},
formLabelWidth: "110px"
};
},
props: {
// 每一列的数据
row: {
type: Object,
default: () => {}
},
canEdit: {
type: Boolean,
default: false
}
},
methods: {
getOrderStatusOptions() {
getData(`/api/order-status`).then(data => {
this.orderStatusOptions = data || [];
});
},
handleColumnClick() {
this.dialogFormVisible = true;
},
// 提交
handleSubmit(formName) {
this.$refs[formName].validate(valid => {
if (!valid) {
util.message.success("校验失败");
return false;
}
// 触发父组件提交函数
this.$emit("updateOrder", this.formRow);
this.dialogFormVisible = false;
});
}
}
};
</script>
注释1即监控dialogFormVisible的变化,为true即编辑弹窗显示时,将父组件传递过来的row赋值给formRow,用于在表单中显示。
综上,我们基本实现了本文开头提出的目标,并在实现功能的同时将Vue.js中一些常用知识点给大家做了简单的介绍。本文涉及的内容比较浅显,但相对广泛,目的是让小伙伴们对Vue项目有个全局的认识,对Vue中的一些知识点有个大概的印象,其中可能也不乏错误和疏漏,仅供入门学习使用。正规项目中,可以通过飞冰、beeworks等搭建,也可去GitHub上下载一些美观且完善的前端项目代码。最后,欢迎小伙伴们批评指正,共同学习。
附件:(还在研究如何上传附件ing)
a) 前端代码:testvue
b) mock接口代码:app.js
c) nginx配置:
server {
listen 8081;
server_name localhost;
location / {
root E:/testvue/dist;
try_files $uri $uri/ /index.html;
index index.html index.htm;
}
location ^~/api {
proxy_pass http://127.0.0.1:3000;
expires -1;
}
}