0
点赞
收藏
分享

微信扫一扫

【实战】电商后台管理系统:路由封装&&基础布局


目录

  • ​​1. 项目介绍​​
  • ​​1.1 项目功能​​
  • ​​1.2 项目架构图​​
  • ​​1.3 项目地址​​
  • ​​2. 路由的二次封装​​
  • ​​3. 页面布局部分​​
  • ​​3.1 主页面布局​​
  • ​​3.2 商品列表功能实现​​

1. 项目介绍

1.1 项目功能

【实战】电商后台管理系统:路由封装&&基础布局_vue

1.2 项目架构图

【实战】电商后台管理系统:路由封装&&基础布局_js_02

1.3 项目地址

补充一下项目地址:

项目演示地址:​​http://mail.cuggz.com​​​ 项目源码:​​https://github.com/cugerGZ/vue-el-admin​​

2. 路由的二次封装

对于整个项目,可能需要使用到很多的路由,那么就需要我们一个个导入组件,并定义路由,这样就很麻烦,我们可以对路由进行二次封装,就可以很方面的进行定义。
先看一下原先定义的好的路由结构(这里已经将routes这部分分离出来,放在了一个单独的配置文件中,在router.js中引入了它):

let routes = [
{
path: '/',
name: 'layout',
component: () => import('../../views/layout.vue'),
redirect: {name: 'index'},
children:[
{
path: '/index',
name: 'index',
component: () => import('../../views/index/index.vue')
},
{
path: '/shop/goods/list',
name: 'shop_goods_list',
component: () => import('../../views/shop/goods/list.vue')
}
]
},
{
path: '/login',
name: 'login',
component: () => import('../../views/login/index.vue')
},
{
path:'*',
redirect: {name: 'index'}
}
]

封装好的路由将满足以下几个条件:

  • 用户只需写一个简单的路径就可以自动生成路由
  • 可以自动生成pash,例如:​​shop/goods/list​​​生成​​/shop/goods/list​
  • 可以自动生成name,例如:​​shop/goods/list​​​生成​​shop_goods_list​
  • 如果路径的最后一个路径值为index,path和name就默认去除index,例如:​​index/index​​​为​​index​
  • 如果用户填写path和name后,就不会自动生成了

在上面我们可以看到,例如:​​import('../../views/shop/goods/list.vue')​​​,和别的相比,除了​​shop/goods/list​​这一部分不一样外,其他部分都一样,那么我们可以将这一部分定义成一个变量,我们只要填写一下页面的路径即可。

路由封装:

let routes = [
{
component: 'layout',
redirect: {name: 'index'},
children:[
{
component: 'index/index'
},
{
component: 'shop/goods/list'
}
]
},
{
component: ('login/index')
},
{
path:'*',
redirect: {name: 'index'}
}
]

// 获取路由信息
let getRoutes = function(){
createRoute(routes)
return routes
}

// 自动生成路由
function createRoute(arr){
for(let i=0;i<arr.length;i++){
// 如果component为空,直接返回
if(!arr[i].component) return
// 去除结尾的index
let val = getValue(arr[i].component)
// 自动生成name和path,如果已经有,就用已经定义好的
arr[i].name = arr[i].name || val.replace(/\//g, '_')
arr[i].path = arr[i].path || `/${val}`
// 自动生成component
let componentFun = import(`../../views/${arr[i].component}.vue`)
arr[i].component = () => componentFun
// 如果有子路由,就继续递归遍历
if(arr[i].children&&arr[i].children.length>0){
createRoute(arr[i].children)
}
}
}
function getValue(str){
// 去除最后一个index,例如login/index将变为login
// 获取最后一个/的索引值
let index = str.lastIndexOf('/')
// 获取最后一个/后面的值
let val = str.substring(index+1,str.length)
// 如果是index,就去除掉
if(val === 'index'){
return str.substring(index,-1)
}
return str
}

// 导出获取路由的方法
export default getRoutes()

3. 页面布局部分

3.1 主页面布局

在主布局开发中,主要有两个比较困难的点,一是两个导航栏的联动,二是面包屑导航的设置。

(1)面包屑导航

结构:

<div class="border-bottom mb-3 bg-white" v-if="bran.length > 0"style="padding: 20px;margin: -20px;">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item v-for="(item,index) in bran" :key="index" :to="{ path: item.path }">{{item.title}}</el-breadcrumb-item>
</el-breadcrumb>
</div>

初始化:

getRouterBran(){
// 获取当前页面的路由地质
let b = this.$route.matched.filter(v=>v.name)
// 初始化一个数组,用来存放当前页面的路由名称、路由地址、标题
let arr = []
b.forEach((v)=>{
// 过滤layout和index
if (v.name === 'index' || v.name === 'layout') return
arr.push({
name:v.name,
path:v.path,
title:v.meta.title
})
})
// 在数组开始追加一个首页的信息
if (arr.length > 0) {
arr.unshift({ name:'index',path:'/index',title:'后台首页' })
}
this.bran = arr
}

当面包屑导航中的路由发生跳转时,进行监听,并将两个导航栏现在的信息保存在本地,是的以后刷新页面时,还能眺回当前页面:

watch: {
'$route'() {
// 本地存储
localStorage.setItem('navActive',JSON.stringify({
top:this.navBar.active || '0',
left:this.slideMenuActive || '0'
}))
this.getRouterBran()
}
}

(2)导航栏

顶部导航栏:

// 触发顶部导航栏的选中
handleSelect(key) {
this.navBar.active = key
// 默认选中跳转到当前激活
this.slideMenuActive = '0'
if (this.slideMenus.length>0) {
this.$router.push({
name:this.slideMenus[this.slideMenuActive].pathname
})
}
}

侧边栏:

// 触发侧边栏的选中
slideSelect(key) {
this.slideMenuActive = key
// 跳转到指定页面
this.$router.push({
name:this.slideMenus[key].pathname
})
}

// 这两个属性放在computed中实时监听
// 侧边栏的激活菜单的实时获取与重置
slideMenuActive:{
get(){
return this.navBar.list[this.navBar.active].subActive || '0'
},
set(val){
this.navBar.list[this.navBar.active].subActive = val
}
},
// 获取侧边栏菜单的列表
slideMenus() {
return this.navBar.list[this.navBar.active].submenu || []
}

// 页面刷新时,初始化选中的菜单
__initNavBar(){
let r = localStorage.getItem('navActive')
if (r) {
r = JSON.parse(r)
this.navBar.active = r.top
this.slideMenuActive = r.left
}
}

3.2 商品列表功能实现

在这写页面中,我们最后需要和后端进行交互的是规格设置(也就是下面那个表格,那个表格是根据上面的规格值,使用sku算法进行排序,自动生成的),所以整个发布商品页面的数据,我们储存在Vuex中,便于多组件的使用。由于我们将页面拆分成了很多的组件,所以使用Vuex来存放,更利于多组件的使用。

在商品列表中,最复杂的部分就是商品发布模块的开发,这也应该是整个系统的中最难的一部分。下面就来看一下这部分的一些难点。

先来看一下这部分需要实现的功能:

【实战】电商后台管理系统:路由封装&&基础布局_拖拽_03

主要有以下功能点:

  • 可以添加规格项
  • 修改规格项的名称
  • 交换规格项的顺序
  • 删除规格项
  • 添加规格值
  • 删除规格值
  • 修改规格值
  • 批量操作
  • 设置规格的相关信息

我们看以看到,一个小小的页面,有这么多的功能,下面就来说一下里面较难的几部分:

(1)实现规格值得拖拽排序

这里的规格值是可以进行拖拽排序的,原生的操作可能很难实现这一点,这里就使用了一个npm插件:​​awe-dnd​​

安装这个插件,再​​mian.js​​​中引入他,再使用它的一个自定义指令​​v-dragging​​即可:

<div class="d-flex align-items-center flex-wrap">
<sku-card-children :type="item.type" v-for="(item2,index2) in list" :key="index2" :item="item2" :index="index2" :cardIndex="index" v-dragging="{item:item2, list:list, group:`skuItem${index}`}"></sku-card-children>
</div>

它有三个属性值:

属性

类型

说明

item

Object

每一个可拖拽的对象

list

Array

可拖拽对象的数组

group

String

这个是dragging list的unique key

实际上,拖拽就是改变拖拽对象在数组中的所以值,所以,我们需要监听它的结束事件,并保存拖拽完成之后的索引值。

mounted(){
// 监听拖拽排序结束
this.$dragging.$on('dragend', (e) => {
// e.group返回的是的拖拖拽的对象所在的规格项的key值
if(e.group === 'skuItem'+ this.index){
this.sortSkuValue({
index: this.index, // 拖拽卡片的索引值
value: this.list // 拖拽之后规格值得列表
})
}
})
}

// 排序规格卡片的规格属性列表,在Vuex中进行修改
sortSkuValue(state, { index, value }) {
state.sku_card[index].list = value
}

(2)交换卡片的排序(规格项)

对于规格项的位置交换,实际上,也是操作卡片对象在数组中的索引值。

下面是交换使用的算法:

swapArray(arr, index1, index2) {
arr[index1] = arr.splice(index2, 1, arr[index1])[0];
return arr;
},
// 上移 将当前数组index索引与前面一个元素互换位置,向数组前面移动一位
moveUp(arr, index) {
this.swapArray(arr, index, index - 1);
},
// 下移 将当前数组index索引与后面一个元素互换位置,向数组后面移动一位
moveDown(arr, index) {
this.swapArray(arr, index, index + 1);
}

以前不是很注意splice这个API,三个参数,第一个参数是需要开始删除的索引值,第二个参数是需要删除的元素的个数,第三个参数是新增一个元素。该方法返回值是包含删除的值的数组。

这里将索引为index2的元素删掉,换成索引为的index1的值,并将删掉的元素赋值给索引为index1的元素。这样就一句代码。完美实现了两个元素的顺序交换。

(3)自动生成下方的规格表格

这应该是最难的一部分了,这个表格主要分别两部分,一部分是上面的表头,在就是表格的身体部分。

先看表头,这里用到了几乎很少用的​​colspan​​​、​​rowspan​​。这两个属性用来合并两个表格项。

【实战】电商后台管理系统:路由封装&&基础布局_数组_04

由于规格项的部分是不确定的,所以要动态获取:

<thead>
<!--渲染上面一行的属性-->
<tr>
<th scope="col" class="text-center"style="vertical-align: middle;" v-for="(th,thi) in tableThs" :key="thi" :rowspan="th.rowspan" :colspan="th.colspan">{{th.name}}</th>
</tr>
<!--渲染下面一行的规格项-->
<tr>
<th scope="col" class="text-center"style="vertical-align: middle;" v-for="(th,thi) in skuLabels" :key="thi" rowspan="1" colspan="1">{{th.name}}</th>
</tr>
</thead>

// 表头数据
ths: [
{ name: "商品规格", rowspan: 1, colspan: 1, width: "" },
{ name: "图片", rowspan: 2, width: "60" },
{ name: "销售价", rowspan: 2, width: "100" },
{ name: "市场价", rowspan: 2, width: "100" },
{ name: "成本价", rowspan: 2, width: "100" },
{ name: "库存", rowspan: 2, width: "100" },
{ name: "体积", rowspan: 2, width: "100" },
{ name: "重量", rowspan: 2, width: "100" },
{ name: "编码", rowspan: 2, width: "100" },
],

// 将规格项进行过滤,没有规格值得规格项不会显示
skuLabels(state) {
return state.sku_card.filter(v => {
return v.list.length > 0
})
},
// 获取表头
tableThs(state, getters) {
let length = getters.skuLabels.length
state.ths[0].colspan = length
state.ths[0].rowspan = length > 0 ? 1 : 2
return state.ths
},

这样表头就渲染完了,下面就该渲染表格项了,这里需要使用sku算法,就所有的规格项进行排序组合。

排序完的效果如下:

【实战】电商后台管理系统:路由封装&&基础布局_拖拽_05


首先是要获取表格数据:

// 获取表格数据
tableData(state) {
if (state.sku_card.length === 0) {
return []
}
let sku_list = [] // 用来储存每个规格项中的规格值列表
for (let i = 0; i < state.sku_card.length; i++) {
if (state.sku_card[i].list.length > 0) {
sku_list.push(state.sku_card[i].list)
}
}
if (sku_list.length === 0) {
return []
}
// 使用sku算法,对数据进行排序
let arr = $Util.cartesianProductOf(...sku_list)
return arr.map(v => {
let obj = {
skus: [], // 包含组合出的规格项
image: "", // 图片
oprice: 0, // 市场价格
pprice: 0, // 销售价格
cprice: 0, // 成本价格
stock: 0, // 库存
weight: 0, // 重量
volume: 0, // 体积
code: '' // 编码
}
obj.skus = v
return obj
})
}

最后的返回值是一个对象:

【实战】电商后台管理系统:路由封装&&基础布局_vue_06

对于上面的示例,返回了四个对象,我们可以看到第一个对象的skus是一个包含两个元素的数组,这是自由组合之后的其中的一个结果。

那么最关键的来了,就是sku算法的实现(实际上就是多个数组,每个数组中的有n个对象,将每个数组的中的对象和其他的数组中的对象进行自由组合):

// sku排列算法
cartesianProductOf() {
return Array.prototype.reduce.call(arguments,function(a, b) {
var ret = [];
a.forEach(function(a) {
b.forEach(function(b) {
ret.push(a.concat([b]));
});
});
return ret;
}, [[]]);
},

在这个算法中,使用到的数组的​​reduce​​方法,该方法中,可以让arguments(参数数组)中的所有的数组项都调用回调函数。回调函数有两个参数,第一参数a是上一次调用回调函数的返回值,第二个参数是本次调用的参数。

这个算法中我们输入的对象如下:

[
[{
name: "黄色",
image: "",
color: ""
},
{
name: "红色",
image: "",
color: ""
}
],
[{
name: "X",
image: "",
color: ""
},
{
name: "XL",
image: "",
color: ""
}
]
]

用展开运算符将数组开展,就是两个数组。输出的结果格式如下:

[
[
{
color: "",
image: "",
name: "黄色"
},
{
color: "",
image: "",
name: "X"
}
],
[
{
color: "",
image: "",
name: "黄色"
},
{
color: "",
image: "",
name: "XL"
}
],
[
{
color: "",
image: "",
name: "红色"
},
{
color: "",
image: "",
name: "X"
}
],
[
{
color: "",
image: "",
name: "红色"
},
{
color: "",
image: "",
name: "XL"
}
],
]

(5)点击添加图片

在多规格里面还有一个功能就是,点击上传图片。上面的规格项会用到图片上传,下面的表格也会用到表格上传。这里我们使用之前设置的商品相册里面的图片,这样也更加便于图片的管理。点击选择之后会弹出以下模态框(这里还没有和后台进行数据交互):

【实战】电商后台管理系统:路由封装&&基础布局_vue_07

由于这个模态框,在后面我们也可能会使用到,所以把他放在了全局中(​​APP.vue​​中),这样,所有的子组件都可以使用它。那么怎么将他传递给子组件呢?这里用到了还从来没用过的Vue的依赖注入。这是Vue中一个比较高阶的知识。

在​​APP.vue​​中:

//依赖注入
provide(){
return {
app: this
}
}

在使用模态框的组件:

// 接收依赖注入
inject:['app'],

这样,在子组件中就可以使用​​APP.vue​​所有的方法了。这也是Vue父子组件船只的一种方式。

由于在​​APP.vue​​​的代码太多,不便于管理,所以将其内部的模态框的相关代码抽离出一个组件​​image-dialog.vue​​。有三个关键的函数,需要区分:

  • 在卡片需要添加图片的组件中:

点击添加按钮,触发该函数,该方法会触发​​APP.vue​​​中的​​chooseImage​​方法,并向其传递一个回调函数

chooseImage(){
this.app.chooseImage((res) => {
this.vModel('image',res[0].url) // 将获取到的图片的url保存在image字段中
},1)
}

  • ​APP.vue​​中

该方法会触发模态框组件中的​​chooseImage​​方法,并向其传递那个回调函数

chooseImage(callback, max= 9 ){
this.maxChooseImage = max
this.$refs.imageDialog.chooseImage(callback) // 触发子组件的方法
}

  • 在模态框组件image-dialog.vue中

// 弹出确认
confirm(){
if(typeof this.callback === 'function'){
this.callback(this.chooseList) // 执行回调函数,并向其传入包含选中的图片的数组
}
this.hide()
},
// 弹出隐藏(关闭)
hide(){
this.imageModal = false
this.callback = false
},
// 选择照片
chooseImage(callback){
this.unChoose() // 清空之前选择的照片
this.callback = callback // 将接收到的回调函数保存
this.imageModal = true // 显示模态框
}

最后执行回调函数,这样就形成了一个闭环,将获取到的图片的链接赋值给image字段,这样就可以在页面显示选中的图片了。


举报

相关推荐

0 条评论