项目仓库:https://github.com/changeclass/vue-shop

主页布局

主页布局使用ElementUI提供的布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
<el-container class="home-container">
<!-- 头部区域 -->
<el-header>
<el-button type="info" @click="logout">退出</el-button>
</el-header>
<!-- 页面主体 -->
<el-container>
<!-- 侧边栏 -->
<el-aside width="200px">Aside</el-aside>
<!-- 页面右侧 -->
<el-main>Main</el-main>
</el-container>
</el-container>

ElementUI提供的组件,其组件名就是他的class名,因此样式可以写成如下

1
2
3
4
5
6
7
8
9
10
11
12
.home-container{
height: 100%;
}
.el-header {
background-color: #373d41;
}
.el-aside {
background-color: #333744;
}
.el-main {
background-color: #eaedf1;
}

header布局

header布局使用flex布局很容易实现。

  1. 对于HTML结构改造成如下

    1
    2
    3
    4
    5
    6
    7
    <el-header>
    <div>
    <img src="../assets/heima.png" alt="">
    <span>电商后台管理系统</span>
    </div>
    <el-button type="info" @click="logout">退出</el-button>
    </el-header>
  2. css样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    .el-header {
    background-color: #373d41;
    display: flex;
    justify-content: space-between;
    padding-left: 0;
    align-items: center;
    color: #fff;
    font-size: 20px;
    >div{
    display: flex;
    align-items: center;
    span{
    margin-left: 15px;
    }
    }
    }

侧边栏布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<el-menu
background-color="#333744"
text-color="#fff"
active-text-color="#ffd04b"
>
<!-- 一级菜单 -->
<el-submenu index="1">
<!-- 一级菜单模板区域 -->
<template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>导航一</span>
</template>
<!-- 二级菜单 -->
<el-menu-item index="1-4-1">
<template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>导航一</span>
</template>
</el-menu-item>
</el-submenu>
</el-menu>

但由于是按需导入的ElementUI组件,因此还需要在注册相关的组件。

1
2
3
4
5
6
7
8
9
10
import {
Menu,
MenuItem,
Submenu,
MenuItemGroup
} from 'element-ui'
Vue.use(Menu)
Vue.use(MenuItem)
Vue.use(Submenu)
Vue.use(MenuItemGroup)

配置请求拦截器

配置请求拦截器的目的主要是因为请求数据时需要进行头部的TOKEN验证,因此在请求拦截器中设置TOKEN的无疑是最好的方式。在入口函数中对axios进行设置

1
2
3
4
5
// 设置拦截器
axios.interceptors.request.use(config => {
config.headers.Authorization = window.sessionStorage.getItem('token')
return config
})

发起请求获取左侧菜单数据

组件一渲染就应该发起请求去获取数据,根据数据将其赋值给自己的属性。根据属性进行UI渲染。

image-20201105092652272

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
created () {
this.getMenuList()
},
data: function () {
return {
menulist: []
}
},
methods: {
async getMenuList () {
const { data: res } = await this.$http.get('menus')
if (res.meta.status !== 200) return this.$message.error(res.meta.message)
this.menulist = res.data
}
}
}

左侧菜单UI绘制

根据接口返回的数据格式,可以使用两个for循环渲染菜单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<el-menu
background-color="#333744"
text-color="#fff"
active-text-color="#ffd04b"
>
<!-- 一级菜单 -->
<el-submenu
:index="item.id + ''"
v-for="item in menulist"
:key="item.id"
>
<!-- 一级菜单模板区域 -->
<template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>{{ item.authName }}</span>
</template>
<!-- 二级菜单 -->
<el-menu-item
:index="subItem.id + ''"
v-for="subItem in item.children"
:key="subItem.id"
>
<template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>{{ subItem.authName }}</span>
</template>
</el-menu-item>
</el-submenu>
</el-menu>

图标

对于二级菜单图标使用的是相同的图标,即写死即可。但一级菜单会根据不同而变化。因此可以定义一个属性用于记录每个分类应展示的图标。

1
2
3
4
5
6
7
8
9
10
11
data: function () {
return {
iconObj: {
125: 'iconfont icon-user',
103: 'iconfont icon-tijikongjian',
101: 'iconfont icon-shangpin',
102: 'iconfont icon-danju',
145: 'iconfont icon-baobiao'
}
}
},

渲染时只需要取出对应的键的值即可。

1
<i :class="iconObj[item.id]"></i>

只能展开一个一级菜单

对menu添加属性unique-openedtrue即可

1
<el-menu :unique-opened="true"></el-menu>

折叠与展开

5cda81b6-4207-4346-81b1-72ec53794d42

组件提供了一个属性collapse用于控制是否折叠展开,但展开与折叠时宽度也要随之变化,因此通过此值也需要变化宽度

1
2
3
4
5
6
7
8
9
10
11
<el-aside :width="isCollpase ? '64px' : '200px'">
<div class="toggle-button" @click="toggleCollapse">|||</div>
<el-menu
background-color="#333744"
text-color="#fff"
active-text-color="#409bff"
:unique-opened="true"
:collapse="isCollpase"
:collapse-transition="false"
></el-menu>
</el-aside>

在data中定义一个属性用于控制当前是否展开

1
2
3
4
5
data: function () {
return {
isCollpase: false
}
},

点击按钮后保持激活状态

ElementUI组件对menu提供一个属性default-active,只需要将其设置为需要高刘高亮的index即可。为了记录当前点击的状态,也将其记录在sessionstore中。并且在每次点击按钮时将当前的路径存储带sessionstore中,组件创建时自动取出其值。

1
<el-menu :default-active="activePath">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
created () {
this.getMenuList()
this.activePath = window.sessionStorage.getItem('activePath')
},
data: function () {
return {
activePath: ''
}
},
methods: {
saveNavState (activePath) {
window.sessionStorage.setItem('activePath', activePath)
this.activePath = activePath
}
}
}

首页重定向

定义一个新的Welcome组件,并对home设置子路由

1
2
3
4
5
6
7
8
9
10
import Welcome from '../components/Welcome.vue'
const routes = [
// home页面
{
path: '/home',
component: Home,
redirect: '/welcome',
children: [{ path: '/welcome', component: Welcome }]
}
]

为每一个子菜单设置路由,ElementUI提供了路由功能,只需要在menu组件中添加属性router即可。点击子菜单会自动转到当前item的id。因此还需要将子菜单的id改为数据返回的path属性(加/

首页主要区域

使用的组件同样需要按需导入。

面包屑导航

1
2
3
4
5
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
<el-breadcrumb-item>用户列表</el-breadcrumb-item>
</el-breadcrumb>

在全局样式中覆盖此组件的样式

1
2
3
4
.el-breadcrumb {
margin-bottom: 15px;
font-size: 12px;
}

卡片视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 卡片视图 -->
<el-card>
<!-- 搜搜与添加 -->
<el-row :gutter="20">
<el-col :span="7">
<el-input placeholder="请输入内容">
<el-button slot="append" icon="el-icon-search"></el-button>
</el-input>
</el-col>
<el-col :span="4">
<el-button type="primary">添加用户</el-button>
</el-col>
</el-row>
</el-card>

同样的为了美观也在全局样式中覆盖一下样式

1
2
3
.el-card {
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15) !important;
}

获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
data () {
return {
queryInfo: {
query: '',
pagenum: 1,
pagesize: 2
},
userlist: [],
total: 0
}
},
created () {
this.getUserList()
},
methods: {
async getUserList () {
const { data: res } = await this.$http.get('users', { params: this.queryInfo })
if (res.meta.status !== 200) return this.$message.error('获取用户信息失败')
this.userlist = res.data.users
this.total = res.data.total
}
}
}

渲染用户数据

渲染用户数据使用table元素,

1
2
3
4
5
6
7
8
9
10
<el-table :data="userlist" border stripe>
<!-- 索引列只需要用type属性即可 -->
<el-table-column type="index" label="#"></el-table-column>
<el-table-column label="姓名" prop="username"></el-table-column>
<el-table-column label="邮箱" prop="email"></el-table-column>
<el-table-column label="电话" prop="mobile"></el-table-column>
<el-table-column label="角色" prop="role_name"></el-table-column>
<el-table-column label="状态" prop="mg_state"></el-table-column>
<el-table-column label="操作"></el-table-column>
</el-table>
  • label

    表格标题

  • prop

    表格列的数据源

  • :data

    表格数据绑定的数据

  • border

    加入边框

  • stripe

    隔行变色

改造状态列显示效果

在单元格中添加一个作用域插槽,其属性slot-cope表示接收的属性

1
2
3
4
5
<el-table-column label="状态" prop="mg_state">
<template v-slot="scope">
<el-switch v-model="scope.row.mg_state"></el-switch>
</template>
</el-table-column>

此时scope相当于当前项。

操作列的改造

基本UI效果以及el-tooltip提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<el-table-column label="操作" width="180px">
<template>
<!-- 修改按钮 -->
<el-button
type="primary"
icon="el-icon-edit"
size="mini"
></el-button>

<!-- 删除按钮 -->
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
></el-button>

<!-- 分配角色按钮 -->
<el-tooltip
effect="dark"
content="分配角色"
placement="top"
:enterable="false"
>
<el-button
type="warning"
icon="el-icon-setting"
size="mini"
></el-button>
</el-tooltip>
</template>
</el-table-column>

分页

https://element.eleme.cn/#/zh-CN/component/pagination

1
2
3
4
5
6
7
8
9
10
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryInfo.pagenum"
:page-sizes="[1, 2, 5, 10]"
:page-size="queryInfo.pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
1
2
3
4
5
6
7
8
9
10
11
12
methods: {
// 改变 pagesize 事件
handleSizeChange (newSize) {
this.queryInfo.pagesize = newSize
this.getUserList()
},
// 监听 页码值 改变的事件
handleCurrentChange (newPage) {
this.queryInfo.pagenum = newPage
this.getUserList()
}
}

用户状态修改

当switch开关被点击后会触发change事件,因此只需要在事件中请求修改状态的API即可。

1
2
3
4
<el-switch
@change="userStateChanged(scope.row)"
v-model="scope.row.mg_state"
></el-switch>
1
2
3
4
5
6
7
8
9
10
11
12
methods: {
async userStateChanged (userinfo) {
console.log(userinfo)
const { data: res } = await this.$http.put(`users/${userinfo.id}/state/${userinfo.mg_state}`)
if (res.meta.status !== 200) {
userinfo.mg_state = !userinfo.mg_state
console.log(res)
return this.$message.error('更新用户状态失败')
}
this.$message.success('更新用户状态成功')
}
}

搜索用户

搜索用户只需要对输入框进行双向数据绑定并提交事件即可。

输入框提供了一个clearable属性,即可以清空输入框,同时触发一个clear事件。

1
2
3
4
5
6
7
8
9
10
11
12
<el-input
placeholder="请输入内容"
v-model="queryInfo.query"
clearable
@clear="getUserList"
>
<el-button
slot="append"
icon="el-icon-search"
@click="getUserList"
></el-button>
</el-input>

添加用户

对话框

1
2
3
4
5
6
7
8
9
10
11
<el-dialog title="提示" :visible.sync="addDialogVisible" width="50%">
<!-- 内容主体 -->
<span>这是一段信息</span>
<!-- 底部区域 -->
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addDialogVisible = false"
>确 定</el-button
>
</span>
</el-dialog>

通过属性addDialogVisible控制对话框的显示与隐藏。

1
<el-button type="primary" @click="addDialogVisible = true" >添加用户</el-button>

绘制提交表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<el-form
:model="addForm"
:rules="addFormRules"
ref="ruleFormRef"
label-width="70px"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="addForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="addForm.password"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="addForm.email"></el-input>
</el-form-item>
<el-form-item label="电话" prop="mobile">
<el-input v-model="addForm.mobile"></el-input>
</el-form-item>
</el-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
data () {
return {
addForm: {
username: '',
password: '',
email: '',
mobile: ''
},
addFormRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 10, message: '请输入3到10位', trigger: 'blur' }],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 15, message: '请输入6到15位', trigger: 'blur' }],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' }],
mobile: [
{ required: true, message: '请输入手机号', trigger: 'blur' }]
}

}
},

自定义验证规则

在data函数中定义函数用于校验规则,然后在验证规则中使用validator关键字调用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
data () {
// 自定义规则 - 邮箱
var checkEmail = (rule, value, cb) => {
// 验证邮箱的正则
const regEmail = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/
if (regEmail.test(value)) {
return cb()
}
cb(new Error('请输入合法的邮箱哦!'))
}
// 自定义规则 - 手机号
var checkMobile = (rule, value, cb) => {
// 验证手机号的正则
const regMobile = /^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/
if (regMobile.test(value)) { return cb() }
cb(new Error('请输入合法的手机号哦!'))
}

return {
// 添加用户表单验证规则
addFormRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 10, message: '请输入3到10位', trigger: 'blur' }],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 15, message: '请输入6到15位', trigger: 'blur' }],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ validator: checkEmail, trigger: 'blur' }],
mobile: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ validator: checkMobile, trigger: 'blur' }]
}

}
},

重置表单

当dialog关闭时会触发close事件,因此为其触发函数重置表单即可。

1
2
3
addDialogClosed () {
this.$refs.ruleFormRef.resetFields()
}
1
<el-dialog @close="addDialogClosed"> </el-dialog>

提交验证

点击确定会触发提交事件,提交前应该预验证是否符合格式要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
addUser () {
this.$refs.ruleFormRef.validate(async valid => {
if (!valid) return
console.log(valid)
// 添加网路请求
const { data: res } = await this.$http.post('users', this.addForm)
if (res.meta.status !== 201) {
this.$message.error('添加用户失败')
}
this.$message.success('添加用户成功')
// 因此对话框
this.addDialogVisible = false
// 重新获取列表
this.getUserList()
})
}
1
<el-button type="primary" @click="addUser">确 定</el-button>

修改用户

修改用户逻辑与添加用户逻辑大致相同,点击编辑按钮打开对话框,此时应根据ID查询相应的数据并填充到表单中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 修改用户并提交
editUserInfo () {
this.$refs.editFormRef.validate(async valid => {
if (!valid) return
// 添加网路请求
const { data: res } = await this.$http.put(`users/${this.editForm.id}`, {
email: this.editForm.email,
mobile: this.editForm.mobile
})
console.log(res)
if (res.meta.status !== 200) {
this.$message.error('修改用户失败')
}
this.$message.success('修改用户成功')
// 隐藏对话框
this.editDialogVisible = false
// 重新获取列表
this.getUserList()
})
},
// 展示编辑用户的对话框
async showEditDialog (val) {
const { data: res } = await this.$http.get(`users/${val.id}`)
if (res.meta.status !== 200) return this.$message.error('查询用户信息失败!')
this.editForm = res.data
this.editDialogVisible = true
console.log(this.editForm)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<el-dialog
title="编辑用户"
:visible.sync="editDialogVisible"
width="50%"
@close="editDialogClosed"
>
<el-form
:model="editForm"
:rules="editFormRules"
ref="editFormRef"
label-width="70px"
>
<el-form-item label="用户名">
<el-input v-model="editForm.username" disabled></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="editForm.email"></el-input>
</el-form-item>
<el-form-item label="电话" prop="mobile">
<el-input v-model="editForm.mobile"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="editUserInfo">确 定</el-button>
</span>
</el-dialog>

删除用户

删除用户应该弹出提示框提示用户是否删除,从而避免误删行为。使用提示框需要全局挂载

image-20201105161456244

1
2
3
4
import {
MessageBox
} from 'element-ui'
Vue.prototype.$confirm = MessageBox.confirm

未删除按钮绑定事件,并将待删除的用户ID传入

1
2
3
4
5
6
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="removeUserById(scopeEdit.row.id)"
></el-button>

定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通过ID删除用户
async removeUserById (id) {
// 弹窗询问是否删除
const confirmResult = await this.$confirm('此操作将永久删除该用户,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'waring'
}).catch(err => err)
// 如果用户确认则返回字符串 - confirm
// 如果用户确认则返回字符串 - cancel
console.log(confirmResult)
if (confirmResult !== 'confirm') {
return this.$message.info('已经取消了删除')
} else {
return this.$message.success('删除了该用户')
}
}

点击取消后会抛出错误,因此我们需要用catch捕获错误并抛出解决报错问题。