去年做的项目,拖了很久,总算是打起精力去做这个项目的总结,并对Vue2的相关知识进行回顾与复习
各个功能模块如果有过多重复冗杂的部分,将会抽取部分值得记录复习的地方进行记录
一:项目简介
前端技术栈
Vue2
vue-router
Element-ui
Axios
Echarts
项目构架
功能模块
用户登录/退出模块
用户管理模块
权限管理模块
角色列表模块
权限列表模块
商品管理模块
商品列表模块
分类管理模块
参数管理模块
订单管理模块
数据统计模块
二:各个功能模块
1:项目初始化
通过vue-cli脚手架进行配置安装
vue2配置
配置vue-router
配置axios
后端接口:http://43.143.0.76:8889/api/private/v1/
在main.js文件配置根路径: axios.defaults.baseURL = ‘http://43.143.0.76:8889/api/private/v1/‘
1.1:路由器的配置
配置路由器,通过login登录之后会优先跳转到home父组件(Element-ui的布局模式),redirect重定向路由到welcome欢迎组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const router = new Router({ routes:[ {path: '/' , redirect: '/login'}, {path: '/login' , component:() => import('@/components/Login.vue')}, { path: '/home' , component:() => import('@/components/Home.vue'), redirect: '/welcome', children: [ { path: '/welcome' , component:() => import('@/components/Welcome.vue') }, { path: '/users' , component:() => import('@/components/user/Users.vue')}, { path: '/rights' , component:() => import('@/components/power/Rights.vue')}, { path: '/roles' , component:() => import('@/components/power/Roles.vue')}, { path: '/categories' , component:() => import('@/components/goods/Cate.vue')}, { path: '/params' , component:() => import('@/components/goods/Params.vue')}, { path: '/goods' , component:() => import('@/components/goods/List.vue')}, { path: '/goods/add' , component: () => import('@/components/goods/Add.vue')}, { path: '/orders' , component: () => import('@/components/order/Order.vue')}, { path: '/reports' , component: () => import('@/components/report/Report.vue')} ] } ] })
|
2:用户登录/退出模块
==相关技术点==
- http是无状态的
- 通过cookie在客户端记录状态
- 通过session在服务器端记录状态
- 通过token方式维持状态
如果前端和后台不存在跨域
问题,可以通过cookie和session来记录登录状态,如果存在跨域
问题,通过token方式来维持登录状态
2.1:登录token原理分析
如果不通过登录来获取服务器的token
值,直接通过路由跳转对应页面,服务器无法验证token通过
,有些接口功能将无法实现,由此还需要配置路由守卫
来防止用户直接通过路由跳转对应页面
2.2:登录login函数逻辑代码
这里是使用了 async 和 await 来解析对应的promise对象。async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
如果登录成功,将服务器的token值保存到客户端的sessionStorage
中,利用seiItem属性键值对的方法存储,以便之后的后续请求都携带token认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| login() { this.$refs.loginFormRef.validate(async (valid) => { if (!valid) { return } else { const { data: res } = await this.$http.post('login', this.loginForm) if (res.meta.status != 200) { this.$message.error('登录失败!') } else { this.$message.success('登录成功!') window.sessionStorage.setItem('token', res.data.token) this.$router.push('/home') } } }) },
|
2.3:路由守卫
在router.js中配置路由守卫,目的是为了防止用户未通过登录,而是选择更改路由跳转到对应页面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| router.beforeEach((to , from , next) => {
if(to.path === '/login'){ next() }else{ const tokenStr = window.sessionStorage.getItem('token') if(!tokenStr){ next('/login') }else{ next() } }
})
|
2.4:Element-ui的表单验证和表单重置
表单验证
:rules属性绑定,data中的表单验证对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <el-form ref="loginFormRef" :model="loginForm" label-width="0px" :rules="loginFormRules" class="login_form"> <el-form-item prop="username"> <el-input v-model="loginForm.username" prefix-icon="el-icon-user"></el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" type="password" prefix-icon="el-icon-lock"></el-input> </el-form-item> <el-form-item class="login_form_login"> <el-button type="primary" @click="login">登录</el-button> <el-button type="info" @click="reset">重置</el-button> </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
| loginFormRules: { username: [ { required: true, message: '请输入登录账户', trigger: 'blur' }, { min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur', }, ], password: [ { required: true, message: '请输入登录密码', trigger: 'blur' }, { min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur', }, ], },
|
表单重置
this指向vue的原型对象,通过原型对象绑定对应的resetFields函数
1 2 3 4 5
| reset() { this.$refs.loginFormRef.resetFields() },
|
2.5:退出登录
直接调用sessionStorage.clear()函数清空存储的token即可,同时跳转到/login页面即可
1 2 3 4
| exit() { window.sessionStorage.clear() this.$router.push('/login') },
|
3:用户管理模块
3.1:Element-ui侧边栏
在回顾具体功能模块前提不得不提一下element-ui的侧边栏组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <el-aside :width="closeValue ? '64px' : '200px' "> <el-menu :default-active="activePath" class="el-menu-vertical-demo" unique-opened :collapse="closeValue" :collapse-transition="false" router> <div class="size" @click="menuClose">| | |</div> <el-submenu :index="item.id + ''" v-for="item in menuList" :key="item.id"> <template slot="title"> <i :class="iconList[item.id]"></i> <span>{{item.authName}}</span> </template> <el-menu-item :index="'/' + subitem.path" v-for="subitem in item.children" :key="subitem.id" @click="saveNavState('/' + subitem.path)"> <i class="el-icon-menu"></i> <span>{{subitem.authName}}</span> </el-menu-item> </el-submenu> </el-menu> </el-aside>
|
- 在el-menu标签中,:default-active=””属性值是激活菜单的值,并没有设死,我们数据绑定到data中的activePath中
- 在el-submenu标签中,:index值是通过父级的v-for属性将menuList的所有item通过l插值语法(两个花括号)显示到模板上,需要注意的一点是,v-for属性同时需要,key属性的同时存在,否则会报错
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 { menuList: [], iconList: { 125: 'el-icon-s-custom', 103: 'el-icon-lock', 101: 'el-icon-shopping-cart-1', 102: 'el-icon-s-order', 145: 'el-icon-s-data', }, closeValue: false, activePath: '', } }, created() { this.getMenuList() this.activePath = window.sessionStorage.getItem('activePath') }, saveNavState(activePath) { window.sessionStorage.setItem('activePath', activePath) this.activePath = activePath },
|
savNavState函数,解决了图标在当前选项高亮,但当重进还是会选择上一高亮位置,但内容则是welcome组件内容
通过sessionStorage存储当前的activePath,在created()组件被创建
的时候再从sessionStorage中取出
1 2 3 4 5
| saveNavState(activePath) { window.sessionStorage.setItem('activePath', activePath) this.activePath = activePath },
|
3.2:作用域插槽
其余的element-ui界面布局组件没什么难度,跟着文档走就可以了,需要回顾的就是slot-scope作用域插槽
这里是想实现一个按钮来切换状态的效果,element-ui提供了这个组件,但是我们同时还要实现,点击切换状态还要修改数据库中的数据
作用域插槽可以理解为:父传子,传结构,根据子组件的中的数据传结构
。
1 2 3 4 5
| <template slot-scope="scope"> <el-switch v-model="scope.row.mg_state" @change="userStateChange(scope.row)"> </el-switch> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12
| async userStateChange(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.$message.error('更新用户状态失败!') } this.$message.success('更新用户状态成功!') },
|
还是利用element-ui组件实现
1 2 3
| <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>
|
分页功能不仅仅这里需要,以后的项目一些业务都是需要分页功能的。
首先需要创建分页数据对象,pagenum(当前页数),pagesize(每页显示数据条数),total(总条数)
。将其返回给后端,后端返回对应数据。
1 2 3 4 5 6 7 8 9
| queryInfo: { query: '', pagenum: 1, pagesize: 2, }, total: 0,
|
3.4:dialog对话框
点击编辑按钮会弹出一个对话框来实现我们的编辑功能
逻辑如下:
- 点击编辑按钮,触发点击事件。展示dialog同时通过id搜索该用户的个人信息,将其展现。
- 用户通过更改本文内容,双向数据绑定到editform表单对象中
- 点击取消,修改visible属性(布尔值)来隐藏该对话框
- 点击确定,通过请求修改对应数据库中信息,同时列表需要刷新,再次调用获取用户数据函数,也修改visible属性隐藏对话框
html结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <el-dialog title="修改用户信息" :visible.sync="editDialogVisible" width="50%" @close="editFormClose"> <el-form :model="editForm" :rules="editFormRules" ref="editFormRef" label-width="100px"> <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="editUser">确 定</el-button> </span> </el-dialog>
|
编辑按钮结构
1 2
| <el-button type="primary" icon="el-icon-edit" circle @click="showEditDialog(scope.row.id)"></el-button>
|
点击确定修改信息逻辑
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
| editUser() { 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, } ) if (res.meta.status != 200) { return this.$message.error('更新用户信息失败!') }
this.editDialogVisible = false this.getUserList() this.$message.success('更新用户信息成功!') }) },
|
4:权限管理模块
4.1:展开表格列
主要还是通过作用域插槽和v-for,还有嵌套的权限属性实现,当然还有tag标签
el-row标签
中利用v-for渲染出父级元素
,蓝色标签,通过作用域插槽传数据,同时el-tag将该值渲染成蓝色标签
- 叉叉移除函数,removeRightById,由于每个权限都有对应的id,所以
通过id来删除数据库中数据
- 嵌套
html结构
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-table-column type="expand"> <template slot-scope="scope"> <el-row :class="['bottom' , i1 === 0 ? 'top' : '','vcenter']" v-for="(item1 ,i1) in scope.row.children" :key="item1.id"> <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 ? '' : 'top','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 type="warning" v-for="(item3) in item2.children" :key="item3.id " closable @close="removeRightById(scope.row , item3.id)">{{item3.authName}}</el-tag> </el-col> </el-row> </el-col> </el-row> <el-row :span="19"></el-row> </template> </el-table-column>
|
removeRightById()函数
- 利用了confirm弹窗组件,全局挂载,
promise对象,需要.then().catch()来解析
发送delete请求的参数,利用到了es6模板字符串用法
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
| async removeRightById(role, roleId) { const confirmResult = await this.$confirm( '此操作将永久删除该文件, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', } ).catch((err) => err)
if (confirmResult !== 'confirm') { return this.$message.info('取消了删除!') } else { const { data: res } = await this.$http.delete( `roles/${role.id}/rights/${roleId}` )
if (res.meta.status !== 200) { return this.$message.error('删除权限失败!') } else { role.children = res.data } } },
|
5:商品管理模块
5.1:级联选择器
element-ui提供的级联选择器,有时候会出现bug,element-ui的版本不断地更新也在修正
- v-model = “selectedCateKeys”数据双向绑定数组
- @change事件,当选中节点变化时触发
- :options绑定商品列表
- :props绑定对象的某个值,实现多级级联选择器
html结构
1 2 3 4 5
| <el-col> <span>选择商品分类:</span> <el-cascader v-model="selectedCateKeys" :options="cateList" @change="handleChange" :props="cateProps"></el-cascader> </el-col>
|
@change事件函数
1 2 3 4
| async handleChange() { this.getParamsData() },
|
1 2 3 4 5 6
| cateProps: { value: 'cat_id', label: 'cat_name', chidren: 'children', },
|
5.2:tabs标签页
tabs标签页
- v-model双向数据绑定对应active选中的数据,这里是many和only
- @tab-click事件监听标签页改变触发
实现选择级联选择器的商品时候,展示对应的动态参数逻辑如下
- 通过级联选择器的handleChange和tabs标签页的handleTabClick两个事件,都调用getParamsData()获取商品参数函数
- 通过每个商品的特定id获取对应的参数信息
结构
1 2
| <el-tabs v-model="activeName" @tab-click="handleTabClick">
|
1 2 3 4
| data(){ activeName: 'many', }
|
- 首先级联选择器的长度如果不是3,即选中的只是一二级菜单就清空,不展示
- 第三级菜单,根据所选分类的ID,和当前所处的面板,获取对应的参数
getParamsData()函数
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 38 39 40 41 42
| async getParamsData() { if (this.selectedCateKeys.length !== 3) { this.selectedCateKeys = [] this.manyTableData = [] this.onlyTableData = [] return } else { const { data: res } = await this.$http.get( `categories/${this.cateId}/attributes`, { params: { sel: this.activeName, }, } ) if (res.meta.status !== 200) { this.$message.error('获取参数列表失败!') } else { res.data.forEach((item) => { item.attr_vals = item.attr_vals item.attr_vals = item.attr_vals ? item.attr_vals.split(',') : [] this.$set(item, 'inputVisible', false) item.inputValue = '' }) if (this.activeName === 'many') { this.manyTableData = res.data } else { this.onlyTableData = res.data } } } },
|
将后台返回的数据,进行forEach遍历存储,还利用了split()分割函数
1 2 3 4 5 6 7 8 9 10
| res.data.forEach((item) => { item.attr_vals = item.attr_vals item.attr_vals = item.attr_vals ? item.attr_vals.split(',') : [] this.$set(item, 'inputVisible', false) item.inputValue = '' })
|
5.3: Tree 树形控件
在商品分类模块中,对于分类名称利用到了tree树形控件,用清晰的层级结构展示信息,可展开或折叠。
- :data数据绑定刀catelist,商品分类列表。
- :columns属性columns纵列分布
- 依旧使用作用域插槽,同时利用了
v-if来控制对应的显示与隐藏,利用表达式的值
结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <tree-table style="margin-top: 15px" :data="catelist" :columns="columns" :selection-type="false" :expand-type="false" show-index index-text="#" border> <template slot="isok" slot-scope="scope"> <i class="el-icon-circle-check" v-if="scope.row.cat_deleted === false" style="color: lightgreen"></i> <i class="el-icon-circle-close" v-else style="color:red"></i> </template> <template slot="order" slot-scope="scope"> <el-tag v-if="scope.row.cat_level === 0">一级</el-tag> <el-tag type="success" v-else-if="scope.row.cat_level === 1">二级</el-tag> <el-tag type="warning" v-else>三级</el-tag> </template> <template slot="opt" slot-scope="scope"> <el-button type="primary" icon="el-icon-edit" size="mini" @click="showEditDialog(scope.row.cat_id)">编辑</el-button> <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeUserById(scope.row.cat_id)">删除</el-button> </template> </tree-table>
|
5.4:添加商品信息模块
5.4.1:el-steps步骤展示信息
1 2 3 4 5 6 7 8 9
| <el-steps :space="200" :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>
|
5.4.2:el-tabs左侧标签页
- @tab-click,当tab标签页被选中时触发事件
结构
1
| <el-tabs v-model="activeIndex" :tab-position="'left'" :before-leave="beforeTabLeave" @tab-click="tabClicked">
|
@tab-click函数
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 38 39
| async tabClicked() { if (this.activeIndex === '1') { const { data: res } = await this.$http.get( `categories/${this.cateId}/attributes`, { params: { sel: 'many' }, } ) if (res.meta.status !== 200) { this.$message.error('获取商品参数失败!') } else { res.data.forEach((item) => { item.attr_vals = item.attr_vals.length === 0 ? [] : item.attr_vals.split(',') }) this.manyTableData = res.data } } else if (this.activeIndex === '2') { const { data: res } = await this.$http.get( `categories/${this.cateId}/attributes`, { params: { sel: 'only' }, } ) if (res.meta.status !== 200) { this.$message.error('获取商品参数失败!') } else { this.onlyTableData = res.data } } },
|
5.4.3:upload上传图片
- :preview:”handlePreview“,处理图片预览函数
- :on-preview=”handlePreview”,处理图片移除函数
1 2 3 4 5 6 7
| <el-tab-pane label="商品图片" name="3"> <el-upload class="upload-demo" action="http://43.143.0.76:8889/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>
|
handlePreview()图片预览函数
1 2 3 4 5 6
| handlePreview(file) { this.previewPath = file.response.data.url this.previewVisible = true },
|
handlePreview()图片移除函数
1 2 3 4 5 6 7 8 9 10 11
| handleRemove(file) { const filePath = file.response.data.tmp_path const i = this.addForm.pics.findIndex((x) => { x.pic === filePath }) this.addForm.pics.splice(i, 1) },
|
6:数据统计模块
6.1:echarts数据报表
1 2 3 4 5 6 7 8
| var myChart = echarts.init(this.$refs.main) const { data: res } = await this.$http.get('reports/type/1') if (res.meta.status !== 200) return this.$message.error('初始化折线图失败!') const data = _.merge(res.data, this.options) myChart.setOption(data)
|
6.2:NProgress的使用
NProgress 是前端轻量级 web 进度条插件
- 导入NProgress包
- 配合axios请求拦截器使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import NProgress from 'nprogress' import 'nprogress/nprogress.css'
axios.interceptors.request.use(config => { NProgress.start() config.headers.Authorization = window.sessionStorage.getItem('token') return config })
axios.interceptors.response.use(config => { NProgress.done() return config })
|
三:总结
总算把一直拖着的项目知识点整理整理完了,ohYeah!!!
回想看完成这个项目,用自己学的vue的知识一步一步的完成,其中有许多相同的部分,但还是自己一步一步的完成了,期间遇到许许多多的error报错但还是上网不断的搜索搜索,debug。
对于路由的掌握更加得心应手了,包括登录的token认证,路由守卫,请求拦截器等等。
但也看过别人的大型项目,是将请求同一封装到一个request,js文件中,这样的好处更多是避免一个请求的方式出错减少更多的修改,以后要还有项目一定尝试尝试。
回想看最头疼的也是最有收获的部分就是分类参数模块的级联选择器配合tabs标签页的使用,添加参数等等。
通过这次的整理也算是对vue2的一个整体复习,要开始步入vue3和ts的学习了。
加油吧!