100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > Vue+Element-UI 电商后台管理系统详细总结

Vue+Element-UI 电商后台管理系统详细总结

时间:2023-11-10 00:30:26

相关推荐

Vue+Element-UI 电商后台管理系统详细总结

一、概述

基于VueElement-UI的电商后台管理系统

1.1 实现功能

用户登录/退出

用户管理

用户列表 实现用户的增删改查、分页查询以及分配角色功能

权限管理

角色列表 实现角色的增删改查以及分配权限、删除权限功能 权限列表 展示所有权限以及权限等级

商品管理

商品列表 实现商品的增删改、分页查询 分类参数 实现分类参数的增删改 商品分类 实现商品分类的增删改以及查看该分类下的所有子分类

订单管理

订单列表 实现修改地址实现查看物流进度

数据统计

数据报表

1.2 前端技术栈

电商后台管理里系统整体采用前后端分离的开发模式,其中前端是基于Vue技术栈的 SPA 项目

vuevue-routerElement-UIAxiosEcharts

1.3 前端项目初始化步骤

安装Vue脚手架通过Vue脚手架创建项目配置Vue路由配置Element-UI组件库配置axios初始化git远程仓库将本地项目托管到Gitee

二、登录与退出

2.1 登录布局

布局代码

<template><div class="login_container"><!-- 登录区域 --><div class="login_box"><div class="logo_box"><img src="../assets/logo.png" alt=""></div><!-- 表单区域 --><el-form class="login_form" ref="loginFormRef" :model="loginForm" :rules="loginFormRules" label-width="0px"><!-- 用户名 --><el-form-item prop="username"><el-input prefix-icon="el-icon-user" v-model="loginForm.username"></el-input></el-form-item><!-- 密码 --><el-form-item prop="password"><el-input prefix-icon="el-icon-lock" v-model="loginForm.password" type="password"></el-input></el-form-item><!-- 按钮 --><el-form-item class="btns"><el-button type="primary" @click="login">登录</el-button><el-button type="info" @click="resetLoginForm">重置</el-button></el-form-item></el-form></div></div></template><style lang="less" scoped>.login_container {background-color: #2b4b6b;height: 100%;}// 登录部分.login_box {width: 450px;height: 300px;background-color: #fff;border-radius: 3px;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);// 图表盒子.logo_box {height: 130px;width: 130px;border: 1px solid #eee;border-radius: 50%;padding: 10px;box-shadow: 0 0 10px #ddd;position: absolute;left: 50%;transform: translate(-50%, -50%);background-color: #fff;img {width: 100%;height: 100%;border-radius: 50%;background-color: #eee;}}// 表单.login_form {position: absolute;bottom: 0;width: 100%;padding: 0 20px;box-sizing: border-box;// 按钮.btns {display: flex;justify-content: flex-end;}}}</style>

实现页面

2.2 登录业务逻辑

2.2.1 表单预验证

用户在输入账号和密码后,点击登录时表单会进行预验证,判断用户输入的账号和密码是否符合规范,验证通过后向服务器发送axios请求

验证规则

// 用户名的验证规则username: [{required: true, message: '请输入用户名称', trigger: 'blur' },{min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }],// 密码的验证规则password: [{required: true, message: '请输入用户密码', trigger: 'blur' },{min: 6, max: 16, message: '长度在 6 到 16 个字符', trigger: 'blur' }]

实现效果

2.2.2 关于 Token

在登录成功后,服务器会向我们返回一个token,我们需要将这个token保存到客户端的sessionStorage

发送请求并保存token

<script>const { data: res } = await this.$http.post('login', this.loginForm)if (res.meta.status === 200) {this.$msg.success('登录成功')window.sessionStorage.setItem('token', res.data.token)this.$router.push('/home')} else {this.$msg.error('用户名或密码输入错误')}</script>

注意

为什么要保存token因为项目中除了登录之外的其他 API 接口必须在登录之后才能访问,在访问的时候携带这个token就证明我们已经登录了为什么要将token保存在sessionStorage因为sessionStorage是会话期间的存储机制,关闭浏览器过后,sessionStorage就会被清空,token只应在当前网站打开期间生效,所以将token保存在sessionStorage

2.2.3 路由导航守卫

如果用户没有登录,但是直接通过 URL 访问特定页面,需要重新导航到登录页面。

index.js中挂载路由导航守卫

// 挂载路由导航守卫// to 代表将要访问的页面路径,from 代表从哪个页面路径跳转而来,next 代表一个放行的函数router.beforeEach((to, from, next) => {// 如果用户访问的是登录页,那么直接放行if (to.path === '/login') return next()// 获取 tokenconst tokenStr = window.sessionStorage.getItem('token')// 没有 token,强制跳转到登录页面if (!tokenStr) return next('/login')next()})

2.3 退出功能

基于token的方式在退出时需要销毁本地的token。这样,后续的请求就不会携带token,必须重新登录生成一个新的token之后才可以访问页面。

退出代码

logout() {// 销毁本地 tokenwindow.sessionStorage.removeItem('token')// 通过编程式导航返回到上一页this.$router.go(-1)}

三、主页部分

3.1 主页布局

引入Element-UI中的HeaderAsideMain组件

样式代码

<style lang="less" scoped>.home_container {height: 100%;}// 头部区域.el-header {display: flex;justify-content: space-between;background-color: #373d41;.el-button {align-self: center;}}// 侧边栏区域.el-aside {background-color: #333744;.el-menu {border-right: none}}// 主题内容区域.el-main {background-color: #eaedf1;}.toggle-button {background-color: #4a5064;font-size: 10px;line-height: 24px;color: #fff;text-align: center;letter-spacing: 0.2em;cursor: pointer;}</style>

实现效果

3.2 菜单部分

向服务器发送axios请求获取菜单数据

注意

需要授权的 API 必须在请求头中使用Authorization字段提供的token令牌,那些授权的 API 才能被正常调用如何在每一个的 API 请求头中添加Authorization字段通过axios请求拦截器添加token,保证拥有获取数据的权限

main.js中添加拦截器

// axios 请求拦截axios.interceptors.request.use(config => {// 为请求头对象,添加 Token 验证的 Authorization 字段config.headers.Authorization = window.sessionStorage.getItem('token')// 最后必须 return configreturn config})

发起请求获取所有菜单数据

<script>methods: {// 获取所有菜单数据async getMenuList() {const { data: res } = await this.$http.get('menus')if (res.meta.status !== 200) return this.$msg.error('获取菜单列表失败')this.menulist = res.data}},created() {this.getMenuList()}</script>

渲染到页面

<el-menubackground-color="#333744"text-color="#fff"active-text-color="#409Eff"<!-- 只保持一个子菜单的展开 -->unique-opened<!-- 水平折叠收起菜单 -->:collapse="isCollapse":collapse-transition="false"router:default-active="activePath"><!-- 一级菜单 --><!-- index 只接收字符串,所以在后面拼接一个空字符串 --><el-submenu :index="item.id + ''" v-for="item in menulist" :key="item.id"><template slot="title"><i :class="iconObj[item.id]"></i><span>{{ item.authName }}</span></template><!-- 二级菜单 --><el-menu-item :index="'/' + secitem.path" v-for="secitem in item.children" :key="secitem.id" @click="savaNavState('/' + secitem.path)"><i class="el-icon-menu"></i><span>{{ secitem.authName }}</span></el-menu-item></el-submenu></el-menu>

通过Element-UI为菜单名称添加图标

实现效果

3.3 用户管理模块

3.3.1 用户列表

1、渲染用户列表

引入Element-UI中的Breadcrumb,BreadcrumbItem,Card,Row,Col组件,实现面包屑导航和卡片视图

样式代码

<!-- 面包屑导航区域 --><el-breadcrumb separator-class="el-icon-arrow-right"><el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item><el-breadcrumb-item>用户管理</el-breadcrumb-item><el-breadcrumb-item>用户列表</el-breadcrumb-item></el-breadcrumb><!-- 卡片区域 --><el-card></el-card><style> .el-breadcrumb {margin-bottom: 15px;font-size: 12px;}.el-card {box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1) !important;}</style>

实现效果

向服务器发送请求获取用户数据列表

<script>data() {return {// 查询参数,实现分页queryInfo: {// 查询字段query: '',// 当前页码./pagenum: 1,// 每页显示的条数./pagesize: 5}}}methods: {// 获取用户列表async getUserList() {const { data: res } = await this.$http.get('users', { params: this.queryInfo })if (res.meta.status !== 200) {return this.$msg.error('获取用户列表失败')}this.userTable = res.data.usersthis.total = res.data.total},// 每页显示条数发生变化触发此事件handleSizeChange(val) {this.queryInfo../pagesize = valthis.getUserList()},// 页码值发生变化触发此事件handleCurrentChange(val) {this.queryInfo../pagenum = valthis.getUserList()}},created() {this.getUserList()}</script>

引入Table,TableColumn将用户数据渲染到表格中,引入Pagination实现分页效果

实现效果

2、 实现用户的增删改查

添加用户

引入Dialog结合表单展示一个添加用户的对话框

实现效果

为表单添加验证规则

<script>data() {// 自定义校验规则// 邮箱验证var checkEmail = (rule, val, cb) => {const regEmail = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/if (regEmail.test(val)) return cb()cb(new Error('请输入合法的邮箱'))}// 手机验证var checkMobile = (rule, val, cb) => {const regMobile = /^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[189]))\d{8}$/if (regMobile.test(val)) return cb()cb(new Error('请输入合法的手机号码'))}return {// 添加用户验证规则addRules: {name: [{ required: true, message: '请输入姓名', trigger: 'blur' },{ min: 2, max: 4, message: '长度在 2 到 4 个字符', trigger: 'blur' }],password: [{ required: true, message: '请输入密码', trigger: 'blur' },{ min: 6, max: 16, message: '长度在 6 到 16 个字符', trigger: 'blur' }],email: [// 验证是否为空{ required: true, message: '请输入邮箱', trigger: 'blur' },// 验证长度{ min: 6, max: 16, message: '长度在 6 到 16 个字符', trigger: 'blur' },// 验证是否合法{ validator: checkEmail, trigger: 'blur' }],mobile: [{ required: true, message: '请输入手机号码', trigger: 'blur' },{ min: 6, max: 16, message: '长度在 6 到 16 个字符', trigger: 'blur' },{ validator: checkMobile, trigger: 'blur' }]}}</script>

实现效果

向服务器发送添加用户请求

<script>export default {data() {return {// 控制添加对话框显示与隐藏dialogVisible: false,// 与表单动态绑定,保存添加用户的数据,将作为参数发送给服务器addForm: {username: '',password: '',email: '',mobile: ''}}},methods: {// 重置添加用户表单addDialogClosed() {this.$refs.addFormRef.resetFields()},// 添加用户addUser() {// 添加用户预验证this.$refs.addFormRef.validate(async valid => {if (!valid) return falseconst { data: res } = await this.$http.post('users', this.addForm)if (res.meta.status !== 201) {if (res.meta.status === 400) return this.$msg.error('用户名已存在')this.$msg.error('用户添加失败')}this.$msg.success('用户添加成功')// 隐藏添加用户的对话框this.dialogVisible = false// 重新获取列表this.getUserList()})}}}</script>

删除用户引入MessageBox提示用户

实现效果

向服务器发送删除用户请求

<template><el-table-column label="操作" width="180"><!-- 添加作用域插槽 --><template slot-scope="scope"><!-- 删除按钮 --><!-- scope.row 就是这一行的数据 --><el-button type="danger" icon="el-icon-delete" size="mini" @click="deleteById(scope.row.id)"></el-button></template></el-table-column></template><script>// 删除用户,点击删除按钮时,将该用户的 id 传过来async deleteById(id) {// 弹框提示是否删除const res = await this.$cfm('此操作将永久删除该用户, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).catch(err => err) // 捕获异常// 如果用户确认删除,则返回值为字符串 confirm// 如果用户取消了删除,则返回值为字符串 cancelif (res !== 'confirm') { return this.$msg.info('已取消删除') }// 如果确认了删除,则发起 axios 删除请求const { data: deleteres } = await this.$http.delete('users/' + id)if (deleteres.meta.status !== 200) {if (deleteres.meta.status === 400) return this.$msg.error('不允许删除admin账户!')return this.$msg.error('删除失败')}this.getUserList()this.$msg.success('删除成功')}</script>

修改用户在点击编辑按钮时候,需要将此用户的 id 传递过来,并且根据此 id 查询用户的信息,然后将用户的信息渲染到表单上

<script>// 展示编辑用户对话框async showEditUser(id) {const { data: res } = await this.$http.get('users/' + id)if (res.meta.status !== 200) return this.$msg.error('查询用户信息失败!')this.editdialogVisible = true// 将查询到的用户信息渲染到表单上this.editForm = res.data}</script>

实现效果

点击确定按钮向服务器发送编辑用户请求

<script>// 编辑用户editUser() {// 用户预验证this.$refs.editFormRef.validate(async valid => {if (!valid) return falseconst { data: res } = await this.$http.put('users/' + this.editForm.id, {email: this.editForm.email,mobile: this.editForm.mobile})if (res.meta.status !== 200) return this.$msg.error('修改用户信息失败!')this.$msg.success('修改用户信息成功!')// 重新获取列表this.getUserList()// 关闭编辑对话框this.editdialogVisible = false})},// 修改用户的状态按钮// 监听 Switch 状态的改变async userStateChanged(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_statethis.$msg.error('用户状态更新失败')}this.$msg.success('用户状态更新成功')}</script>

查询用户查询用户就是将用户信息queryInfo中的query属性和输入框动态绑定,然后向服务器发送获取用户列表的请求

3.4 权限管理模块

3.4.1 权限列表

布局和用户列表一致

向服务器发送请求获取权限数据列表

<script>data() {return {// 权限列表数据rightsTable: []}},methods: {async getRightsList() {const { data: res } = await this.$http.get('rights/list')if (res.meta.status !== 200) return this.$msg.error('获取权限列表失败!')this.rightsTable = res.data}},created() {this.getRightsList()}</script>

实现效果

3.4.2 角色列表

1、渲染角色列表

布局和用户列表一致

向服务器发送请求获取角色数据列表

<script>methods: {// 获取角色列表async getRoleList() {const { data: res } = await this.$http.get('roles')if (res.meta.status !== 200) return this.$msg.error('获取角色列表失败!')this.roleTable = res.data}},created() {this.getRoleList()}</script>

实现效果

2、分配角色

使用Dialog组件结合表单展示一个分配角色的对话框引入SelectOption组件展示一个选择角色的下拉选择器展示对话框之前先向服务器发送请求获取角色列表数据

具体代码

<script>// 展示分配角色对话框async showSetRolesDialog(userinfo) {this.setRoleInfo = userinfoconst { data: res } = await this.$http.get('roles')if (res.meta.status !== 200) return this.$msg.error('获取角色列表失败')this.rolesList = res.datathis.setRolesDialogVisible = true}</script>

将获取过来的角色列表数据展示到分配角色对话框的表单中

实现效果

向服务器发送分配角色请求

<script>// 提交分配角色async commitSetRoles() {if (!this.selectRoleId) return this.$msg.error('请选择要分配的角色')const { data: res } = await this.$http.put(`users/${this.setRoleInfo.id}/role`, {rid: this.selectRoleId})if (res.meta.status !== 200) return this.$msg.error('分配角色失败')this.$msg.success('分配角色成功')this.getUserList()this.setRolesDialogVisible = false},// 关闭分配角色对话框后的操作closeSetRoleDialog() {this.setRoleInfo = ''this.selectRoleId = ''}</script>

3、实现角色的增删改

和用户的增删改查一致,只是调用接口不一样。

4、展示当前角色下的所有权限

当用户点击某个角色的下拉箭头时,该角色的所有权限数据会以类似于树结构的形式展示出来。

用户也可以删除该角色下的某个权限。

效果如图

为表格设置 type=“expand” 和Scoped slot可以开启展开行功能,el-table-column的模板会被渲染成为展开行的内容,展开行可访问的属性与使用自定义列模板时的Scoped slot相同。通过scope.row可以获取该行也就是该角色的数据

<!-- 展开列 --><el-table-column type="expand"><template slot-scope="scope">{{ scope.row }}</template></el-table-column>

效果如图

布局思路

引入Element-UI中的 Layout 布局,可以实现基础的 24 分栏,迅速简便地创建布局。

业务逻辑

通过scope.row获取的数据就是该角色的所有信息,数据是一个对象,每一个对象下都有一个children属性,这个children属性就是该角色的所有权限了,children是一个数组,每一个children属性下又嵌套这一个children属性,一共嵌套三层,这分别就是该角色下的一级、二级、三级权限了。可以循环children下的每个对象,就可以把一级权限渲染出来,在每一个一级权限中又嵌套着二级权限,所以,要想渲染出所有的一级、二级、三级权限需要使用三层v-for循环的嵌套。

具体实现

引入Tag组件将权限名称以标签的形式展示,并且将closable设置为true,每个权限标签后面就会显示一个叉号,为后面的删除权限功能做铺垫。

为每一个权限标签后面添加<i class="el-icon-caret-right"></i>进行美化。

<!-- 展开列 --><el-table-column type="expand"><template slot-scope="scope"><el-row :class="['bdbottom', i1 === 0 ? 'bdtop' : '', 'vcenter']" v-for="(item1,i1) in scope.row.children" :key="item1.id" class="first-row"><!-- 渲染一级权限 --><el-col :span="5"><el-tag closable @close="removeRightById(scope.row, item1.id)">{{ item1.authName }}</el-tag><i class="el-icon-caret-right"></i></el-col><!-- 渲染二级权限 --><el-col :span="19"><el-row :class="[i2 === 0 ? '' : 'bdtop', 'vcenter']" v-for="(item2,i2) in item1.children" :key="item2.id"><el-col :span="6"><el-tag type="success" closable @close="removeRightById(scope.row, item2.id)">{{ item2.authName }}</el-tag><i class="el-icon-caret-right"></i></el-col><!-- 渲染三级权限 --><el-col :span="18"><el-tag v-for="item3 in item2.children" :key="item3.id" type="warning" closable @close="removeRightById(scope.row, item3.id)">{{ item3.authName }}</el-tag></el-col></el-row></el-col></el-row></template></el-table-column><style>// 添加边框// 上边框.bdtop {border-top: 1px solid #eee;}// 下边框.bdbottom {border-bottom: 1px solid #eee;}// 上下居中.vcenter {display: flex;align-items: center;}</style>

效果如图

5、删除权限

使用MessageBox提示用户

点击确定按钮时分别将该角色的信息和权限 id 作为参数传递过来

<script>// 删除权限async removeRightById(role, rightId) {const res = await this.$cfm('此操作将永久删除该权限, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).catch(err => err)if (res !== 'confirm') return this.$msg.info('已取消删除')// 发送请求const { data: res1 } = await this.$http.delete(`roles/${role.id}/rights/${rightId}`)if (res1.meta.status !== 200) return this.$msg.error('删除权限失败!')role.children = res1.datathis.$msg.success('删除权限成功!')}</srcipt>

注意

在发送请求成功后不要调用获取角色列表数据的方法,这样整个列表都会刷新,下拉列也会收上,用户体验感差;应该将服务器返回过来的数据赋值给该角色下的children属性:role.children = res1.data

6、分配权限

布局

使用Dialog组件展示一个分配权限的对话框

弹出对话框之前向服务器发送请求获取所有权限

<script>// 展示分配权限的对话框async showRightsDialog(role) {this.defKeys = []this.setRightsUserId = role.idconst { data: res } = await this.$http.get('rights/tree')if (res.meta.status !== 200) return this.$msg.error('获取权限列表失败!')this.rightsTree = res.data// 递归获取三级节点的 idthis.getLaefKeys(role, this.defKeys)this.setRightsDialogVisible = true},// 通过递归的形式,获取角色下所有三级权限的 id,并保存到 defKeys 数组中getLaefKeys(node, arr) {// 如果当前 node 节点不包含 children 属性,那么这个节点就是三级节点if (!node.children) {return arr.push(node.id)}node.children.forEach(item => this.getLaefKeys(item, arr))}</script>

引用Tag组件将权限列表渲染成树形结构

<template><!-- 分配权限对话框 --><el-dialogtitle="分配权限":visible.sync="setRightsDialogVisible"width="50%"><el-tree :data="rightsTree" :props="treeProps" show-checkbox node-key="id" :default-expand-all="true" :default-checked-keys="defKeys" ref="treeRef"></el-tree><span slot="footer" class="dialog-footer"><el-button @click="setRightsDialogVisible = false">取 消</el-button><el-button type="primary" @click="commitSetRights">确 定</el-button></span></el-dialog></template>

效果如下

提交分配权限

展示分配权限对话框时需要获取当前角色下的所有权限,在树结构中默认勾选上。

<script>// 提交分配的权限async commitSetRights() {// ... 数组合并const setRights = [// getCheckedKeys() 返回目前被选中的节点的 key(id)所组成的数组...this.$refs.treeRef.getCheckedKeys(),// getHalfCheckedKeys() 返回目前半选中的节点所组成的数组...this.$refs.treeRef.getHalfCheckedKeys()]// 将数组转换为用逗号分隔的字符串const str = setRights.join(',')const { data: res } = await this.$http.post(`roles/${this.setRightsUserId}/rights`, { rids: str })if (res.meta.status !== 200) return this.$msg.error('分配权限失败')this.$msg.success('分配权限成功')// 重新获取权限列表数据this.getRoleList()// 关闭对话框this.setRightsDialogVisible = false}</script>

3.5 商品管理模块

3.5.1 商品分类

1、渲染商品分类列表

布局

和角色列表布局一致

表格部分引用第三方组件vue-table-with-tree-grid(树形表格组件),可以使商品分类以树形结构分级展示

安装vue-table-with-tree-grid

npm i vue-table-with-tree-grid -S

main.js中引入vue-table-with-tree-grid

// 引入列表树import TreeTable from 'vue-table-with-tree-grid'// 使用列表树ponent('tree-table', TreeTable)

以组件组件标签的形式使用vue-table-with-tree-grid

<template><!-- data 绑定的是数据源;columns 为 table 指定的列 --><tree-table :data="catelist" :columns="columns":selection-type="false" :expand-type="false"show-index index-text="#" border:show-row-hover="false"></tree-table></template>

向服务器发送请求获取商品分类数据列表

<script>// 获取商品分类数据async getCateList() {const { data: res } = await this.$http.get('categories', {params: this.queryInfo})if (res.meta.status !== 200) return this.$msg.error('获取商品分类列表失败')// 将获取过来的数据赋值给表格绑定的数据源this.catelist = res.data.result},created() {this.getCateList()}</script>

效果如图

设置自定义列

设置自定义列需要将columns绑定的对应列的type属性设置为template,将template属性设置为当前列使用的模板名称

<script>columns: [{label: '分类名称',prop: 'cat_name'},{label: '是否有效',// 表示将当前列定义为模板列type: 'template',// 表示当前这一列使用的模板名称template: 'isok'},{label: '排序',// 表示将当前列定义为模板列type: 'template',// 表示当前这一列使用的模板名称template: 'order'},{label: '操作',// 表示将当前列定义为模板列type: 'template',// 表示当前这一列使用的模板名称template: 'opt'}]</script>

<template><!-- 是否有效 列 --><!-- slot 绑定的是当前列模板的名称 --><template slot="isok" slot-scope="scope"><i class="el-icon-success" v-if="scope.row.cat_deleted === false" style="color: lightgreen;"></i><i class="el-icon-error" v-else style="color: red;"></i></template><!-- 排序 列 --><template slot="order" slot-scope="scope"><el-tag size="mini" v-if="scope.row.cat_level === 0">一级</el-tag><el-tag type="success" size="mini" v-else-if="scope.row.cat_level === 1">二级</el-tag><el-tag type="warning" size="mini" v-else>三级</el-tag></template><!-- 操作 列 --><template slot="opt"><el-button icon="el-icon-edit" size="mini" type="primary">编辑</el-button><el-button icon="el-icon-delete" size="mini" type="danger">删除</el-button></template></template>

效果如下

2、实现商品的增删改

添加分类

使用Dialog组件结合表单展示一个添加分类的对话框

引用Cascader级联选择器组件展示所有商品分类

<template><!-- 添加分类对话框 --><el-dialogtitle="提示":visible.sync="addCateDialogVisible"width="50%"><!-- 添加分类的表单 --><el-form ref="addCateFormRef" :model="addCateForm" :rules="addCateRules" label-width="100px"><el-form-item label="分类名称" prop="cat_name"><el-input v-model="addCateForm.cat_name"></el-input></el-form-item><el-form-item label="父级分类"><!-- 级联选择器 --><!-- options 绑定的数据源 --><el-cascader :options="parentCateList" clearable:props="parentCateListProps" v-model="selectKeys"@change="parentCateChanged"></el-cascader></el-form-item></el-form><span slot="footer" class="dialog-footer"><el-button @click="addCateDialogVisible = false">取 消</el-button><el-button type="primary" @click="addCateDialogVisible = false">确 定</el-button></span></el-dialog></template>

效果如图

向服务器发送请求获取所有父级分类商品

将服务器返回的数据赋值给级联选择器绑定的数据源

<script>// 获取父级分类的列表数据async getParentCateList() {const { data: res } = await this.$http.get('categories', {// type 参数指定为2,就是获取父级分类商品params: { type: 2 }})if (res.meta.status !== 200) return this.$msg.error('获取列表数据失败')this.parentCateList = res.data}</script>

效果如图

编辑分类

使用 Dialog 对话框展示一个编辑分类的对话框

<template><!-- 编辑分类对话框 --><el-dialogtitle="编辑分类":visible.sync="editCateDialogVisible"width="50%" @close="closeEditCateDialog"><!-- 编辑分类的表单 --><el-form ref="editCateFormRef" :model="editCateForm" :rules="editCateRules"label-width="100px"><el-form-item label="分类名称" prop="cat_name"><el-input v-model="editCateForm.cat_name"></el-input></el-form-item></el-form><span slot="footer" class="dialog-footer"><el-button @click="editCateDialogVisible = false">取 消</el-button><el-button type="primary" @click="commitEditCate">确 定</el-button></span></el-dialog></template>

使用作用域插槽,当点击编辑按钮时,获取当前分类的 id,将这个 id 作为参数向服务器发起编辑请求,获取当前分类的名称并展示到表单的输入框中

<script>// 展示编辑分类的对话框async showEditCateDialogVisible(id) {this.editCateDialogVisible = trueconst { data: res } = await this.$http.get(`categories/${id}`)if (res.meta.status !== 200) return this.$msg.error('查询分类数据失败')this.editCateForm.cat_name = res.data.cat_namethis.editCateId = res.data.cat_id}</script>

效果如图

点击确定按钮向服务器发送请求提交此次编辑操作

<script>// 提交编辑分类async commitEditCate() {const { data: res } = await this.$http.put(`categories/${this.editCateId}`, this.editCateForm)if (res.meta.status !== 200) return this.$msg.error('编辑分类失败')this.$msg.success('编辑分类成功')this.getCateList()this.editCateDialogVisible = false}</script>

删除分类

使用MessageBox对话框提示用户再次确认

使用作用域插槽,当点击删除按钮时,获取当前分类的 id,当点击确定按钮时,将这个 id 作为参数向服务器发起删除请求

<script>// 删除分类async deleteEditCateById(id) {// 弹框提示是否删除const res = await this.$cfm('此操作将永久删除该分类, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).catch(err => err) // 捕获异常// 如果用户确认删除,则返回值为字符串 confirm// 如果用户取消了删除,则返回值为字符串 cancelif (res !== 'confirm') { return this.$msg.info('已取消删除') }const { data: res1 } = await this.$http.delete(`categories/${id}`)if (res1.meta.status !== 200) return this.$msg.error('删除分类失败')this.$msg.success('删除成功')this.getCateList()}</script>

3.5.2 分类参数

1、渲染分类参数布局

布局

也是使用卡片布局

引用Alert组件提示用户

引用Tabs标签页组件实现动态参数页和静态属性页的局部切换

<template><!-- 卡片试图区域 --><el-card><!-- 警告区域 --><el-alerttitle="注意:只允许为第三级分类设置相关参数!"type="warning":closable="false"show-icon></el-alert><!-- 选择商品分类区域 --><el-row class="cat_opt"><el-col><span>选择商品分类:</span><!-- 级联选择器 --><el-cascaderv-model="selectKeys":options="catelist"@change="handleChange"></el-cascader></el-col></el-row><!-- Tab 页签区域 --><el-tabs v-model="activeName" @tab-click="handleTabClick"><!-- 动态参数页签 --><el-tab-pane label="动态参数" name="many"><el-button type="primary" size="mini"@click="showAddDialogVisible">添加参数</el-button><!-- 动态参数表格 --><el-table><!-- 展开列 --><el-table-column type="expand"></el-table-column><!-- 索引列 --><el-table-column type="index"></el-table-column><el-table-column label="参数名称" prop="attr_name"></el-table-column><el-table-column label="操作"><template slot-scope="scope"><el-button type="primary" size="mini" icon="el-icon-edit">编辑</el-button><el-button type="danger" size="mini" icon="el-icon-delete" @click="deleteParamsById(scope.row.attr_id)">删除</el-button></template></el-table-column></el-table></el-tab-pane><!-- 静态属性页签 --><el-tab-pane label="静态属性" name="only"><el-button type="primary" size="mini"@click="showAddDialogVisible">添加属性</el-button><!-- 静态属性表格 --><el-table><!-- 展开列 --><el-table-column type="expand"></el-table-column><!-- 索引列 --><el-table-column type="index"></el-table-column><el-table-column label="属性名称" prop="attr_name"></el-table-column><el-table-column label="操作"><template slot-scope="scope"><el-button type="primary" size="mini" icon="el-icon-edit">编辑</el-button><el-button type="danger" size="mini" icon="el-icon-delete" @click="deleteParamsById(scope.row.attr_id)">删除</el-button></template></el-table-column></el-table></el-tab-pane></el-tabs></el-card></template>

效果如图

获取商品分类列表

获取商品分类列表,并渲染到级联选择器当中

<script>// 获取商品分类列表async getCateList() {const { data: res } = await this.$http.get('categories')if (res.meta.status !== 200) return this.$msg.error('获取商品分类失败')// 将获取过来的数据赋值给级联选择器绑定的数据源this.catelist = res.data}</script>

当用户选中商品分类时,向服务器发送请求获取商品参数了列表并在表格中展示该分类的所有参数

<script>// 当级联选择器选中项发生变化,会触发这个函数handleChange() {this.getCateParams()},// 获取参数列表async getCateParams() {// 判断是否选择的是三级分类if (this.selectKeys.length !== 3) {// this.$msg.warning('只能选择三级分类')this.selectKeys = []} else {const { data: res } = await this.$http.get(`categories/${this.cateId}/attributes`, {params: { sel: this.activeName }})if (res.meta.status !== 200) return this.$msg.error('获取参数列表失败')if (this.activeName === 'many') {this.manyTableDate = res.data} else {this.onlyTableName = res.data}}}</script>

效果如图

2、实现分类参数的增删改

添加分类参数

分类参数包括动态参数和静态属性

当用户点击添加参数/属性时,弹出对话框

因为添加参数和添加属性的对话框布局一样,所以可以共用一个对话框

<template><!-- 添加参数/属性共用一个对话框 --><el-dialog:title="'添加' + titleText":visible.sync="addDialogVisible"width="30%"@closed="handleClose"><el-form ref="addFormRef" :model="addForm" label-width="120px" :rules="addFormRules"><el-form-item :label="titleText + '名称'" prop="attr_name"><el-input v-model="addForm.attr_name"></el-input></el-form-item></el-form><span slot="footer" class="dialog-footer"><el-button @click="addDialogVisible = false">取 消</el-button><el-button type="primary" @click="commitAdd">确 定</el-button></span></el-dialog></template><script>computed: {// 对话框标题titleText() {if (this.activeName === 'many') return '动态参数'return '静态属性'}}</script>

提交添加操作

<script>// 提交添加操作commitAdd() {// 表单预验证this.$refs.addFormRef.validate(async valid => {if (!valid) return falseconst { data: res } = await this.$http.post(`categories/${this.cateId}/attributes`, {attr_name: this.addForm.attr_name,attr_sel: this.activeName})if (res.meta.status !== 201) return this.$msg.error('添加失败')this.$msg.success('添加成功')this.getCateParams()this.addDialogVisible = false})}</script>

删除分类参数

当用户点击删除按钮时,通过作用域插槽获取当前分类参数的id

提交删除操作

<script>// 根据 id 删除参数async deleteParamsById(id) {// 弹框提示是否删除const res = await this.$cfm('此操作将永久删除该分类, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).catch(err => err) // 捕获异常// 如果用户确认删除,则返回值为字符串 confirm// 如果用户取消了删除,则返回值为字符串 cancelif (res !== 'confirm') { return this.$msg.info('已取消删除') }const { data: res1 } = await this.$http.delete(`categories/${this.cateId}/attributes/${id}`)if (res1.meta.status !== 200) return this.$msg.error('删除失败')this.$msg.success('删除成功')this.getCateParams()}</script>

编辑分类参数

点击编辑按钮后,将要编辑的分类参数额id传递过来将id作为参数向服务器发送请求获取当前要编辑的分类参数的名称将获取过来的名称展示到编辑对话框的输入框中展示编辑对话框

<template><el-table-column label="操作"><template slot-scope="scope"><el-button type="primary" size="mini" icon="el-icon-edit" @click="showEditDialog(scope.row.attr_id)">编辑</el-button></template></el-table-column></template><script>// 显示编辑对话框async showEditDialog(id) {const { data: res } = await this.$http.get(`categories/${this.cateId}/attributes/${id}`, {params: { attr_sel: this.activeName }})if (res.meta.status !== 200) return this.$msg.error(`${res.meta.msg}`)this.editForm = res.datathis.editDialogVisible = true}</script>

效果如图

提交编辑操作

<script>// 提交编辑操作commitEdit() {// 表单预验证this.$refs.editFormRef.validate(async valid => {if (!valid) return falseconst { data: res1 } = await this.$http.put(`categories/${this.cateId}/attributes/${this.editForm.attr_id}`, {attr_name: this.editForm.attr_name,attr_sel: this.activeName})if (res1.meta.status !== 200) return this.$msg.error('编辑失败')this.$msg.success(`${res1.meta.msg}`)this.getCateParams()this.editDialogVisible = false})}</script>

3、添加删除分类参数的属性

添加分类参数的属性

在表格展开列当中,引入Tag组件,循环渲染每一个标签

<template><!-- 展开列 --><el-table-column type="expand"><template slot-scope="scope"><!-- 循环渲染每一个标签 --><el-tag v-for="(item, i) in scope.row.attr_vals" :key="i" closable @close="deleteTag(i, scope.row)">{{ item }}</el-tag><!-- 添加标签的输入框 --><el-inputclass="input-new-tag"v-if="scope.row.inputVisible"v-model="scope.row.inputValue"ref="saveTagInput"size="small"@keyup.enter.native="handleInputConfirm(scope.row)"@blur="handleInputConfirm(scope.row)"></el-input><!-- 添加按钮 --><el-button v-else class="button-new-tag" size="small" @click="showInput(scope.row)">+ New Tag</el-button></template></el-table-column></template>

效果如图

点击+ New Tag按钮展示文本框,并且隐藏按钮当文本框失去焦点或者用户按下Enter键后,向服务器发送请求,提交此次添加操作

<script>// 文本框失去焦点或按下空格键触发这个函数async handleInputConfirm(row) {if (row.inputValue.trim().length === 0) {row.inputValue = ''row.inputVisible = falsereturn false}// 如果没用 return 出去,就证明用户输入了内容// 数组追加属性row.attr_vals.push(row.inputValue.trim())row.inputValue = ''row.inputVisible = falsethis.saveAttrVal(row)},// 将保存属性的方法抽取出来async saveAttrVal(row) {const { data: res } = await this.$http.put(`categories/${this.cateId}/attributes/${row.attr_id}`, {attr_name: row.attr_name,attr_sel: this.activeName,attr_vals: row.attr_vals.join(' ')})if (res.meta.status !== 200) return this.$msg.error('更新参数属性失败')this.$msg.success('更新参数属性成功')}</script>

删除分类参数的属性

点击叉号,将当前标签的索引作为参数传递过来根据传递过来的索引将该标签移除重新调用保存属性的方法

<script>// 删除标签deleteTag(i, row) {// 根据传递过来的索引删除row.attr_vals.splice(i, 1)this.saveAttrVal(row)}</script>

3.5.3商品列表

1、渲染商品列表

布局

采用面包屑导航加卡片布局向服务器发送请求获取商品列表数据将获取过来的数据渲染到列表当中

<template><!-- 表格区域 --><el-table :data="goodsTable" stripe border><el-table-column type="index" label="#"></el-table-column><el-table-column prop="goods_name" label="商品名称"></el-table-column><el-table-column prop="goods_price" label="商品价格(元)" width="120px"></el-table-column><el-table-column prop="goods_weight" label="商品重量" width="90px"></el-table-column><el-table-column prop="add_time" label="创建时间" width="160px"><template slot-scope="scope">{{ scope.row.add_time | dateFormat }}</template></el-table-column><el-table-column label="操作" width="130px"><template slot-scope="scope"><!-- 编辑按钮 --><el-button type="primary" icon="el-icon-edit" size="mini"></el-button><!-- 删除按钮 --><el-button type="danger" icon="el-icon-delete" size="mini" @click="deleteGoodsById(scope.row.goods_id)"></el-button></template></el-table-column></el-table></template><script>// 获取商品列表async getGoodsTable() {const { data: res } = await this.$http.get('goods', { params: this.queryInfo })// console.log(res)if (res.meta.status !== 200) return this.$msg.error('获取商品列表失败')this.goodsTable = res.data.goodsthis.total = res.data.total},created() {this.getGoodsTable()}</script>

添加分页导航

效果如图

2、查询商品

用户点击查询按钮后,将输入框里的数据作为参数向服务器发送请求

3、删除商品

用户点击删除按钮后,将该商品对应的id作为参数传递过来将此id作为参数向服务器发送删除请求

<script>// 根据 id 删除商品async deleteGoodsById(id) {// 弹框提示是否删除const res = await this.$cfm('此操作将永久删除该分类, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).catch(err => err) // 捕获异常// 如果用户确认删除,则返回值为字符串 confirm// 如果用户取消了删除,则返回值为字符串 cancelif (res !== 'confirm') { return this.$msg.info('已取消删除') }const { data: res1 } = await this.$http.delete(`goods/${id}`)if (res1.meta.status !== 200) return this.$msg.error('删除商品失败')this.$msg.success('商品删除成功')this.getGoodsTable()}</script>

4、添加商品

创建一个添加商品组件,并注册路由规则点击添加商品按钮跳转到此页面

添加商品页面布局

整体采用面包屑导航加卡片视图布局引用Step步骤条组件,使用Tab标签页在基本信息标签页中添加表单

<template><!-- 卡片区域 --><el-card><!-- 提示消息框 --><el-alerttitle="添加商品信息"type="info"centershow-icon:closable="false"></el-alert><!-- 步骤条 --><el-steps :active="activeIndex - 0" finish-status="success" align-center><el-step title="基本信息"></el-step><el-step title="商品参数"></el-step><el-step title="商品属性"></el-step><el-step title="商品图片"></el-step><el-step title="商品内容"></el-step><el-step title="完成"></el-step></el-steps><!-- 标签页区域 --><el-form :model="addForm" :rules="addRules" ref="addFormRef" label-width="100px" label-position="top"><el-tabs v-model="activeIndex" tab-position="left" :before-leave="beforeTabLeave" @tab-click="tabClicked"><el-tab-pane label="基本信息" name="0"><el-form-item label="商品名称" prop="goods_name"><el-input v-model="addForm.goods_name"></el-input></el-form-item><el-form-item label="商品价格" prop="goods_price"><el-input v-model="addForm.goods_price" type="number"></el-input></el-form-item><el-form-item label="商品数量" prop="goods_number"><el-input v-model="addForm.goods_number" type="number"></el-input></el-form-item><el-form-item label="商品重量" prop="goods_weight"><el-input v-model="addForm.goods_weight" type="number"></el-input></el-form-item><el-form-item label="商品分类" prop="goods_weight"><!-- 级联选择器 --><el-cascaderv-model="addForm.goods_cat":options="catelist":props="{ expandTrigger: 'hover', ...cateProps }"@change="handleChange"clearable></el-cascader></el-form-item></el-tab-pane></el-tabs></el-form></el-card></template>

效果如图

在商品参数标签页引入checkbox-group复选框组组件展示参数 先循环动态参数表格数据的每一项,将属性名称展示到页面上再循环表格数据中的属性项,通过复选框组渲染到页面

<template><el-tab-pane label="商品参数" name="1"><el-form-item :label="item.attr_name" v-for="item in manyTableData" :key="item.attr_id"><!-- 复选框组 --><el-checkbox-group v-model="item.attr_vals"><el-checkbox :label="cb" v-for="(cb, i) in item.attr_vals" :key="i" border></el-checkbox></el-checkbox-group></el-form-item></el-tab-pane></template>

效果如图

在商品属性标签页循环静态属性表格数据的每一项,并展示到输入框当中

<template><el-tab-pane label="商品属性" name="2"><el-form-item :label="item.attr_name" v-for="item in onlyTableData" :key="item.attr_id"><el-input v-model="item.attr_vals"></el-input></el-form-item></el-tab-pane></template>

效果如图

在商品图片标签页Upload上传组件实现图片上传的功能

<template><el-tab-pane label="商品图片" name="3"><!-- 上传图片 --><!-- action 表示图片要上传到的后台 API 地址 --><el-uploadaction="http://127.0.0.1:8888/api/private/v1/upload":on-preview="handlePreview":on-remove="handleRemove"list-type="picture":headers="headerObj":on-success="handleSuccess"><el-button size="small" type="primary">点击上传</el-button></el-upload></el-tab-pane></template>

效果如图

在商品内容标签页引入vue-quill-editor富文本编辑器插件在main.js中导入并注册

// 导入富文本编辑器import VueQuillEditor from 'vue-quill-editor'// 导入富文本编辑器对应的样式import 'quill/dist/quill.core.css' // import stylesimport 'quill/dist/quill.snow.css' // for snow themeimport 'quill/dist/quill.bubble.css' // for bubble theme// 使用富文本编辑器Vue.use(VueQuillEditor)

<template><el-tab-pane label="商品内容" name="4"><quill-editor v-model="addForm.goods_introduce"></quill-editor><el-button type="primary" class="addBtn" @click="addGoods">添加商品</el-button></el-tab-pane></template>

效果如图

提交添加商品操作

<script>// 添加商品async addGoods() {this.$refs.addFormRef.validate(valid => {if (!valid) return this.$msg.error('请填写必要的表单项')})// 执行添加的业务逻辑// _.cloneDeep(obj) 深拷贝// 因为级联选择器绑定的数据源格式必须是数组,但是向服务器发送请求传递参数的格式是字符串// 所以进行深拷贝const form = _.cloneDeep(this.addForm)form.goods_cat = form.goods_cat.join(',')// 处理动态参数this.manyTableData.forEach(item => {const newInfo = {attr_id: item.attr_id,attr_value: item.attr_vals.join(' ')}this.addForm.attrs.push(newInfo)})// 处理静态属性this.onlyTableData.forEach(item => {const newInfo = {attr_id: item.attr_id,attr_value: item.attr_vals}this.addForm.attrs.push(newInfo)})form.attrs = this.addForm.attrs// 发起添加商品请求// 商品名称只能是唯一的const { data: res } = await this.$http.post('goods', form)if (res.meta.status !== 201) return this.$msg.error(`${res.meta.msg}`)this.$msg.success('商品添加成功')this.$router.push('/goods')}</script>

3.6 订单管理模块

3.6.1 订单列表

1、渲染订单列表

布局采用面包屑导航加卡片布局向服务器发送请求获取商品列表数据将获取过来的数据渲染到列表当中添加分页导航

<temaplate><!-- 表格区域 --><el-table :data="orderlist" border stripe><el-table-column type="index" label="#"></el-table-column><el-table-column label="订单编号" prop="order_number" width="600px"></el-table-column><el-table-column label="订单价格" prop="order_price" width="95px"></el-table-column><el-table-column label="是否付款" prop="pay_status" width="85px"><template slot-scope="scope"><el-tag v-if="scope.row.pay_status === '1'">已付款</el-tag><el-tag type="danger" v-else>未付款</el-tag></template></el-table-column><el-table-column label="是否发货" prop="is_send" width="95px"></el-table-column><el-table-column label="下单时间" prop="create_time"><template slot-scope="scope">{{ scope.row.create_time | dateFormat }}</template></el-table-column><el-table-column label="操作"><template><el-button size="mini" type="primary" class="el-icon-edit" @click="showEditLocationDialog"></el-button><el-button size="mini" type="success" class="el-icon-location" @click="showProcessDialog"></el-button></template></el-table-column></el-table><!-- 分页区域 --><el-pagination@size-change="handleSizeChange"@current-change="handleCurrentChange":current-./page="queryInfo../pagenum":./page-sizes="[1, 5, 10]":./page-size="queryInfo../pagesize"layout="total, sizes, prev, ./pager, next, jumper":total="total"></el-pagination></temaplate><script>data() {return {queryInfo: {query: '',// 当前页码./pagenum: 1,// 每页显示条数./pagesize: 10},total: 0,// 订单数据orderlist: []}},methods: {// 获取订单列表数据async getOrderList() {const { data: res } = await this.$http.get('orders', {params: this.queryInfo})if (res.meta.status !== 200) return this.$msg.error('获取订单列表失败')this.total = res.data.totalthis.orderlist = res.data.goods},// 每页显示条数发生变化触发这个函数handleSizeChange(val) {this.queryInfo../pagesize = valthis.getOrderList()},// 当前页发生变化触发这个函数handleCurrentChange(val) {this.queryInfo../pagenum = valthis.getOrderList()}},created() {this.getOrderList()}</script>

效果如图

2、修改地址

点击修改按钮,弹出修改地址对话框导入citydata.js包,渲染到表单的级联选择器当中

<template><!-- 修改地址的对话框 --><el-dialogtitle="修改地址":visible.sync="editAddressDialogVisible"width="50%"@close="addressDialogClosed"><el-form :model="addressForm" :rules="addressRules" ref="addressRef" label-width="100px"><el-form-item label="省市区/县" prop="address1"><el-cascader :options="cityData" v-model="addressForm.address1" :props="{ expandTrigger: 'hover' }"></el-cascader></el-form-item><el-form-item label="详细地址" prop="address2"><el-input v-model="addressForm.address2"></el-input></el-form-item></el-form><span slot="footer"><el-button @click="editAddressDialogVisible = false">取 消</el-button><el-button type="primary" @click="editAddressDialogVisible = false">确 定</el-button></span></el-dialog></template><script>import cityData from './citydata'data() {return {// 控制修改地址对话框的显示与隐藏editAddressDialogVisible: false,addressForm: {address1: [],address2: ''},addressRules: {address1: {required: true, message: '请选择省市区/县', trigger: 'blur'},address2: {required: true, message: '请输入详细地址', trigger: 'blur'}},// 省市区/县数据cityData}},methods: {// 显示修改地址对话框showEditLocationDialog() {this.editAddressDialogVisible = true},// 修改地址对话框关闭触发addressDialogClosed() {this.$refs.addressRef.resetFields()}}</script>

效果如图

3、查看物流进度

引入Timeline时间线组件因为查看物流进度的API无法使用,这里使用了Mock.js根据接口文档的响应数据模拟了查看物流进度的接口

// 使用 Mockvar Mock = require('mockjs')var menuMock = Mock.mock({data: [{time: '-05-10 09:39:00',ftime: '-05-10 09:39:00',context: '已签收,感谢使用顺丰,期待再次为您服务',location: ''},{time: '-05-10 08:23:00',ftime: '-05-10 08:23:00',context: '[北京市]北京海淀育新小区营业点派件员 顺丰速运 95338正在为您派件',location: ''},{time: '-05-10 07:32:00',ftime: '-05-10 07:32:00',context: '快件到达 [北京海淀育新小区营业点]',location: ''},{time: '-05-10 02:03:00',ftime: '-05-10 02:03:00',context: '快件在[北京顺义集散中心]已装车,准备发往 [北京海淀育新小区营业点]',location: ''},{time: '-05-09 23:05:00',ftime: '-05-09 23:05:00',context: '快件到达 [北京顺义集散中心]',location: ''},{time: '-05-09 21:21:00',ftime: '-05-09 21:21:00',context: '快件在[北京宝胜营业点]已装车,准备发往 [北京顺义集散中心]',location: ''},{time: '-05-09 13:07:00',ftime: '-05-09 13:07:00',context: '顺丰速运 已收取快件',location: ''},{time: '-05-09 12:25:03',ftime: '-05-09 12:25:03',context: '卖家发货',location: ''},{time: '-05-09 12:22:24',ftime: '-05-09 12:22:24',context: '您的订单将由HLA(北京海淀区清河中街店)门店安排发货。',location: ''},{time: '-05-08 21:36:04',ftime: '-05-08 21:36:04',context: '商品已经下单',location: ''}],meta: {status: 200, message: '获取物流信息成功!' }})Mock.mock('http://127.0.0.1:8888/api/private/v1/mock/process', 'get', menuMock)

点击查看物流进度的按钮,发送请求,并渲染到时间线中

<template><!-- 显示物流进度的对话框 --><el-dialogtitle="物流进度":visible.sync="processDialogVisible"width="50%"><!-- 时间线 --><el-timeline><el-timeline-itemv-for="(item, index) in processData":key="index":timestamp="item.time">{{item.context}}</el-timeline-item></el-timeline></el-dialog></template><script>// 显示物流进度的对话框async showProcessDialog() {const { data: res } = await this.$http.get('/mock/process')if (res.meta.status !== 200) return this.$msg.error('获取物流信息失败')this.processData = res.datathis.processDialogVisible = true}</script>

效果如图

3.7 数据统计模块

3.7.1 数据报表

引入Apache ECharts数据可视化插件向服务器发起数据请求将请求回来的数据与现有数据进行合并展示数据

<script>// 引入 echartsimport * as echarts from 'echarts'import _ from 'lodash'export default {data() {return {// 需要合并的数据options: {title: {text: '用户来源'},tooltip: {trigger: 'axis',axisPointer: {type: 'cross',label: {backgroundColor: '#E9EEF3'}}},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},xAxis: [{boundaryGap: false}],yAxis: [{type: 'value'}]}}},// 此时页面上的元素已经加载完毕了async mounted() {// 基于准备好的dom,初始化echarts实例var myChart = echarts.init(document.getElementById('main'))// 发起请求const { data: res } = await this.$http.get('reports/type/1')if (res.meta.status !== 200) return this.$message.error('获取折线图数据失败')// 准备数据和配置数据项// 调用 lodash 的 merge() 方法,将 res.data 和 this.options 合并成一个新的数据对象const result = _.merge(res.data, this.options)// 展示数据myChart.setOption(result)}}</script>

效果如图

四、项目优化

1.1 项目优化策略

生成打包报告

通过命令行参数的形式生成报告

// 通过 vue-cli 的命令选项可以生成打包报告// --report 选项可以生成 report.html 以帮助分析打包内容vue-cli-service build --report

通过可视化的 UI 面板直接查看报告推荐

在可视化的 UI 面板中,通过**控制台**和分析面板,可以方便地看到项目中所存在的问题

第三方库启用 CDN

通过 externals 加载外部 CDN 资源

默认情况下,通过 import 语法导入的第三方依赖包最终会被打包合并到同一个文件中,从而导致打包成功后,单文件体积过大大的问题。

为了解决上述的问题,可以通过 webpack 的 externals 节点,来配置并加载外部的 CDN 资源。

凡是声明在 externals 中的第三方依赖包,都不会被打包。

module.exports = {chainWebpack:config=>{//发布模式config.when(process.env.NODE_ENV === 'production',config=>{//entry找到默认的打包入口,调用clear则是删除默认的打包入口//add添加新的打包入口config.entry('app').clear().add('./src/main-prod.js')//使用externals设置排除项config.set('externals',{vue:'Vue','vue-router':'VueRouter',axios:'axios',lodash:'_',echarts:'echarts',nprogress:'NProgress','vue-quill-editor':'VueQuillEditor'})})//开发模式config.when(process.env.NODE_ENV === 'development',config=>{config.entry('app').clear().add('./src/main-dev.js')})}

设置好排除之后,为了使我们可以使用vue,axios等内容,我们需要加载外部CDN的形式解决引入依赖项。

打开开发入口文件main-prod.js,删除掉默认的引入代码

import Vue from 'vue'import App from './App.vue'import router from './router'// import './plugins/element.js'//导入字体图标import './assets/fonts/iconfont.css'//导入全局样式import './assets/css/global.css'//导入第三方组件vue-table-with-tree-gridimport TreeTable from 'vue-table-with-tree-grid'//导入进度条插件import NProgress from 'nprogress'//导入进度条样式// import 'nprogress/nprogress.css'// //导入axiosimport axios from 'axios'// //导入vue-quill-editor(富文本编辑器)import VueQuillEditor from 'vue-quill-editor'// //导入vue-quill-editor的样式// import 'quill/dist/quill.core.css'// import 'quill/dist/quill.snow.css'// import 'quill/dist/quill.bubble.css'axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'//请求在到达服务器之前,先会调用use中的这个回调函数来添加请求头信息axios.interceptors.request.use(config => {//当进入request拦截器,表示发送了请求,我们就开启进度条NProgress.start()//为请求头对象,添加token验证的Authorization字段config.headers.Authorization = window.sessionStorage.getItem("token")//必须返回configreturn config})//在response拦截器中,隐藏进度条axios.interceptors.response.use(config =>{//当进入response拦截器,表示请求已经结束,我们就结束进度条NProgress.done()return config})Vue.prototype.$http = axiosVue.config.productionTip = false//全局注册组件ponent('tree-table', TreeTable)//全局注册富文本组件Vue.use(VueQuillEditor)//创建过滤器将秒数过滤为年月日,时分秒Vue.filter('dateFormat',function(originVal){const dt = new Date(originVal)const y = dt.getFullYear()const m = (dt.getMonth()+1+'').padStart(2,'0')const d = (dt.getDate()+'').padStart(2,'0')const hh = (dt.getHours()+'').padStart(2,'0')const mm = (dt.getMinutes()+'').padStart(2,'0')const ss = (dt.getSeconds()+'').padStart(2,'0')return `${y}-${m}-${d} ${hh}:${mm}:${ss}`})new Vue({router,render: h => h(App)}).$mount('#app')

打开index.html添加外部cdn引入代码

<!DOCTYPE html><html lang="en"><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>电商后台管理系统</title><!-- nprogress 的样式表文件 --><link rel="stylesheet" href="/nprogress/0.2.0/nprogress.min.css" /><!-- 富文本编辑器 的样式表文件 --><link rel="stylesheet" href="/quill/1.3.4/quill.core.min.css" /><link rel="stylesheet" href="/quill/1.3.4/quill.snow.min.css" /><link rel="stylesheet" href="/quill/1.3.4/quill.bubble.min.css" /><!-- element-ui 的样式表文件 --><link rel="stylesheet" href="/element-ui/2.8.2/theme-chalk/index.css" /><script src="/vue/2.5.22/vue.min.js"></script><script src="/vue-router/3.0.1/vue-router.min.js"></script><script src="/axios/0.18.0/axios.min.js"></script><script src="/lodash.js/4.17.11/lodash.min.js"></script><script src="/echarts/4.1.0/echarts.min.js"></script><script src="/nprogress/0.2.0/nprogress.min.js"></script><!-- 富文本编辑器的 js 文件 --><script src="/quill/1.3.4/quill.min.js"></script><script src="/npm/vue-quill-editor@3.0.4/dist/vue-quill-editor.js"></script><!-- element-ui 的 js 文件 --><script src="/element-ui/2.8.2/index.js"></script></head><body><noscript><strong>We're sorry but vue_shop doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><!-- built files will be auto injected --></body></html>

Element-UI 组件按需加载

路由懒加载

首页内容定制

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。