项目仓库: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布局使用flex布局很容易实现。
对于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>
|
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渲染。
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-opened
为true
即可
1
| <el-menu :unique-opened="true"></el-menu>
|
折叠与展开
组件提供了一个属性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 = [ { 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> <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: { 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>
|
删除用户
删除用户应该弹出提示框提示用户是否删除,从而避免误删行为。使用提示框需要全局挂载
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
| async removeUserById (id) { const confirmResult = await this.$confirm('此操作将永久删除该用户,是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'waring' }).catch(err => err) console.log(confirmResult) if (confirmResult !== 'confirm') { return this.$message.info('已经取消了删除') } else { return this.$message.success('删除了该用户') } }
|
点击取消后会抛出错误,因此我们需要用catch
捕获错误并抛出解决报错问题。