赞
踩
基于
Vue
和Element-UI
的电商后台管理系统
用户登录/退出
用户管理
权限管理
商品管理
订单管理
数据统计
电商后台管理里系统整体采用前后端分离的开发模式,其中前端是基于
Vue
技术栈的 SPA 项目
Vue
脚手架Vue
脚手架创建项目Vue
路由Element-UI
组件库axios
库git
远程仓库Gitee
中布局代码
<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>
实现页面
用户在输入账号和密码后,点击登录时表单会进行预验证,判断用户输入的账号和密码是否符合规范,验证通过后向服务器发送
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' }
]
实现效果
在登录成功后,服务器会向我们返回一个
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
token
就证明我们已经登录了token
保存在 sessionStorage
中
sessionStorage
是会话期间的存储机制,关闭浏览器过后, sessionStorage
就会被清空,token
只应在当前网站打开期间生效,所以将 token
保存在 sessionStorage
中如果用户没有登录,但是直接通过 URL 访问特定页面,需要重新导航到登录页面。
在 index.js
中挂载路由导航守卫
// 挂载路由导航守卫
// to 代表将要访问的页面路径,from 代表从哪个页面路径跳转而来,next 代表一个放行的函数
router.beforeEach((to, from, next) => {
// 如果用户访问的是登录页,那么直接放行
if (to.path === '/login') return next()
// 获取 token
const tokenStr = window.sessionStorage.getItem('token')
// 没有 token,强制跳转到登录页面
if (!tokenStr) return next('/login')
next()
})
基于
token
的方式在退出时需要销毁本地的token
。这样,后续的请求就不会携带token
,必须重新登录生成一个新的token
之后才可以访问页面。
退出代码
logout() {
// 销毁本地 token
window.sessionStorage.removeItem('token')
// 通过编程式导航返回到上一页
this.$router.go(-1)
}
引入
Element-UI
中的Header
Aside
Main
组件
样式代码
<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>
实现效果
向服务器发送
axios
请求获取菜单数据
注意
Authorization
字段提供的 token
令牌,那些授权的 API 才能被正常调用Authorization
字段
axios
请求拦截器添加 token
,保证拥有获取数据的权限在 main.js
中添加拦截器
// axios 请求拦截
axios.interceptors.request.use(config => {
// 为请求头对象,添加 Token 验证的 Authorization 字段
config.headers.Authorization = window.sessionStorage.getItem('token')
// 最后必须 return config
return 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-menu background-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
为菜单名称添加图标
实现效果
引入
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.users this.total = res.data.total }, // 每页显示条数发生变化触发此事件 handleSizeChange(val) { this.queryInfo../pagesize = val this.getUserList() }, // 页码值发生变化触发此事件 handleCurrentChange(val) { this.queryInfo../pagenum = val this.getUserList() } }, created() { this.getUserList() } </script>
引入
Table
,TableColumn
将用户数据渲染到表格中,引入Pagination
实现分页效果
实现效果
引入
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 false const { 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 // 如果用户取消了删除,则返回值为字符串 cancel if (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>
<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 false const { 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_state this.$msg.error('用户状态更新失败') } this.$msg.success('用户状态更新成功') } </script>
queryInfo
中的 query
属性和输入框动态绑定,然后向服务器发送获取用户列表的请求布局和用户列表一致
向服务器发送请求获取权限数据列表
<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>
实现效果
布局和用户列表一致
向服务器发送请求获取角色数据列表
<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>
实现效果
Dialog
组件结合表单展示一个分配角色的对话框Select
、Option
组件展示一个选择角色的下拉选择器具体代码
<script>
// 展示分配角色对话框
async showSetRolesDialog(userinfo) {
this.setRoleInfo = userinfo
const { data: res } = await this.$http.get('roles')
if (res.meta.status !== 200) return this.$msg.error('获取角色列表失败')
this.rolesList = res.data
this.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>
和用户的增删改查一致,只是调用接口不一样。
当用户点击某个角色的下拉箭头时,该角色的所有权限数据会以类似于树结构的形式展示出来。
用户也可以删除该角色下的某个权限。
效果如图
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>
效果如图
使用
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.data this.$msg.success('删除权限成功!') } </srcipt>
注意:
children
属性:
role.children = res1.data
布局
使用 Dialog
组件展示一个分配权限的对话框
弹出对话框之前向服务器发送请求获取所有权限
<script> // 展示分配权限的对话框 async showRightsDialog(role) { this.defKeys = [] this.setRightsUserId = role.id const { data: res } = await this.$http.get('rights/tree') if (res.meta.status !== 200) return this.$msg.error('获取权限列表失败!') this.rightsTree = res.data // 递归获取三级节点的 id this.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-dialog
title="分配权限"
: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>
布局
和角色列表布局一致
表格部分引用第三方组件 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'
// 使用列表树
Vue.component('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>
效果如下
添加分类
使用 Dialog
组件结合表单展示一个添加分类的对话框
引用 Cascader
级联选择器组件展示所有商品分类
<template> <!-- 添加分类对话框 --> <el-dialog title="提示" :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-dialog title="编辑分类" :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 = true
const { data: res } = await this.$http.get(`categories/${id}`)
if (res.meta.status !== 200) return this.$msg.error('查询分类数据失败')
this.editCateForm.cat_name = res.data.cat_name
this.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 // 如果用户取消了删除,则返回值为字符串 cancel if (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>
布局
也是使用卡片布局
引用 Alert
组件提示用户
引用 Tabs
标签页组件实现动态参数页和静态属性页的局部切换
<template> <!-- 卡片试图区域 --> <el-card> <!-- 警告区域 --> <el-alert title="注意:只允许为第三级分类设置相关参数!" type="warning" :closable="false" show-icon> </el-alert> <!-- 选择商品分类区域 --> <el-row class="cat_opt"> <el-col> <span>选择商品分类:</span> <!-- 级联选择器 --> <el-cascader v-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>
效果如图
添加分类参数
分类参数包括动态参数和静态属性
当用户点击添加参数/属性时,弹出对话框
因为添加参数和添加属性的对话框布局一样,所以可以共用一个对话框
<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 false const { 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 // 如果用户取消了删除,则返回值为字符串 cancel if (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.data this.editDialogVisible = true } </script>
效果如图
<script> // 提交编辑操作 commitEdit() { // 表单预验证 this.$refs.editFormRef.validate(async valid => { if (!valid) return false const { 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>
添加分类参数的属性
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-input class="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 = false return false } // 如果没用 return 出去,就证明用户输入了内容 // 数组追加属性 row.attr_vals.push(row.inputValue.trim()) row.inputValue = '' row.inputVisible = false this.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>
布局
<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.goods this.total = res.data.total }, created() { this.getGoodsTable() } </script>
效果如图
id
作为参数传递过来id
作为参数向服务器发送删除请求<script> // 根据 id 删除商品 async deleteGoodsById(id) { // 弹框提示是否删除 const res = await this.$cfm('此操作将永久删除该分类, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).catch(err => err) // 捕获异常 // 如果用户确认删除,则返回值为字符串 confirm // 如果用户取消了删除,则返回值为字符串 cancel if (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>
添加商品页面布局
Step
步骤条组件,使用 Tab
标签页<template> <!-- 卡片区域 --> <el-card> <!-- 提示消息框 --> <el-alert title="添加商品信息" type="info" center show-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-cascader v-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-upload
action="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 styles
import 'quill/dist/quill.snow.css' // for snow theme
import '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>
<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.total this.orderlist = res.data.goods }, // 每页显示条数发生变化触发这个函数 handleSizeChange(val) { this.queryInfo../pagesize = val this.getOrderList() }, // 当前页发生变化触发这个函数 handleCurrentChange(val) { this.queryInfo../pagenum = val this.getOrderList() } }, created() { this.getOrderList() } </script>
效果如图
citydata.js
包,渲染到表单的级联选择器当中<template> <!-- 修改地址的对话框 --> <el-dialog title="修改地址" :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>
效果如图
Timeline
时间线组件API
无法使用,这里使用了 Mock.js
根据接口文档的响应数据模拟了查看物流进度的接口// 使用 Mock var Mock = require('mockjs') var menuMock = Mock.mock({ data: [ { time: '2018-05-10 09:39:00', ftime: '2018-05-10 09:39:00', context: '已签收,感谢使用顺丰,期待再次为您服务', location: '' }, { time: '2018-05-10 08:23:00', ftime: '2018-05-10 08:23:00', context: '[北京市]北京海淀育新小区营业点派件员 顺丰速运 95338正在为您派件', location: '' }, { time: '2018-05-10 07:32:00', ftime: '2018-05-10 07:32:00', context: '快件到达 [北京海淀育新小区营业点]', location: '' }, { time: '2018-05-10 02:03:00', ftime: '2018-05-10 02:03:00', context: '快件在[北京顺义集散中心]已装车,准备发往 [北京海淀育新小区营业点]', location: '' }, { time: '2018-05-09 23:05:00', ftime: '2018-05-09 23:05:00', context: '快件到达 [北京顺义集散中心]', location: '' }, { time: '2018-05-09 21:21:00', ftime: '2018-05-09 21:21:00', context: '快件在[北京宝胜营业点]已装车,准备发往 [北京顺义集散中心]', location: '' }, { time: '2018-05-09 13:07:00', ftime: '2018-05-09 13:07:00', context: '顺丰速运 已收取快件', location: '' }, { time: '2018-05-09 12:25:03', ftime: '2018-05-09 12:25:03', context: '卖家发货', location: '' }, { time: '2018-05-09 12:22:24', ftime: '2018-05-09 12:22:24', context: '您的订单将由HLA(北京海淀区清河中街店)门店安排发货。', location: '' }, { time: '2018-05-08 21:36:04', ftime: '2018-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-dialog title="物流进度" :visible.sync="processDialogVisible" width="50%"> <!-- 时间线 --> <el-timeline> <el-timeline-item v-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.data this.processDialogVisible = true } </script>
效果如图
Apache ECharts
数据可视化插件<script> // 引入 echarts import * 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>
效果如图
生成打包报告
通过命令行参数的形式生成报告
// 通过 vue-cli 的命令选项可以生成打包报告
// --report 选项可以生成 report.html 以帮助分析打包内容
vue-cli-service build --report
通过可视化的 UI 面板直接查看报告 推荐
在可视化的 UI 面板中,通过**控制台**和分析面板,可以方便地看到项目中所存在的问题
通过 externals 加载外部 CDN 资源
默认情况下,通过 import 语法导入的第三方依赖包最终会被打包合并到同一个文件中,从而导致打包成功后,单文件体积过大大的问题。
为了解决上述的问题,可以通过 webpack 的 externals 节点,来配置并加载外部的 CDN 资源。
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-grid import TreeTable from 'vue-table-with-tree-grid' //导入进度条插件 import NProgress from 'nprogress' //导入进度条样式 // import 'nprogress/nprogress.css' // //导入axios import 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") //必须返回config return config }) //在response拦截器中,隐藏进度条 axios.interceptors.response.use(config =>{ //当进入response拦截器,表示请求已经结束,我们就结束进度条 NProgress.done() return config }) Vue.prototype.$http = axios Vue.config.productionTip = false //全局注册组件 Vue.component('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')
<!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="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.css" /> <!-- 富文本编辑器 的样式表文件 --> <link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.core.min.css" /> <link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.snow.min.css" /> <link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.bubble.min.css" /> <!-- element-ui 的样式表文件 --> <link rel="stylesheet" href="https://cdn.staticfile.org/element-ui/2.8.2/theme-chalk/index.css" /> <script src="https://cdn.staticfile.org/vue/2.5.22/vue.min.js"></script> <script src="https://cdn.staticfile.org/vue-router/3.0.1/vue-router.min.js"></script> <script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script> <script src="https://cdn.staticfile.org/lodash.js/4.17.11/lodash.min.js"></script> <script src="https://cdn.staticfile.org/echarts/4.1.0/echarts.min.js"></script> <script src="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.js"></script> <!-- 富文本编辑器的 js 文件 --> <script src="https://cdn.staticfile.org/quill/1.3.4/quill.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue-quill-editor@3.0.4/dist/vue-quill-editor.js"></script> <!-- element-ui 的 js 文件 --> <script src="https://cdn.staticfile.org/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 组件按需加载
路由懒加载
首页内容定制
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。