后台“尚品汇”大型购物商城项目
视频链接:
https://www.bilibili.com/video/BV1Vf4y1T7bw?spm_id_from=333.337.search-card.all.click
1. 后台项目涉及到的页面
- 登录页面
- 首页数据收集与展示页面
- 商品管理页面
- 品牌管理
- 平台属性管理
- Sku管理
- Spu管理
- 权限管理
目录展示:
2. Spu管理页面
1. 页面展示
1. 主页面展示:
这个功能页面中,涉及到的数据比较多,下面是它代码展示:
data() {
//这里存放数据
return {
//这仨是三级分类各自的Id
category1Id: "",
category2Id: "",
category3Id: "",
//这仨是分页器用到的
page: 1,
limit: 3,
total: 0, //分页器总共的页数
record: [], //存储spu列表的数据
//这个可以控制各个组件的显示与隐藏
scene: 0, //0代表展示spu数据,1添加修改spu 2 添加sku
spu: {},
skuList: [], //存储的是sku列表的数据
dialogVisible: false,
loading: true,
};
},
2. 添加spu界面
整个页面的布局,是由一个表单元素el-form包裹起来的,里面的每一项则是用el-form-item表单子元素包裹的。
有几点需要注意的:
- 照片墙,这里可以显示多个照片,下面是他的代码
<!-- 上传图片
list-type:文件列表的类型
on-preview:图片预览的时候会触发
on-remove:图片删除的时候会触发
file-list:照片墙需要展示的数据[数组里面的元素必须要有name、url属性]-->
<el-upload
action="/dev-api/admin/product/fileUpload"
list-type="picture-card"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
:file-list="spuImageList"
:on-success="handlersuccess"
>
<i class="el-icon-plus"></i>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
在这个照片墙中呢,其中的file-list数组,必须要有Name还有url属性,因此获取到服务器数据之后,要再进一步的处理,
- 下拉框,这里在向后台发起请求,获得销售属性之后,要计算出来用户还有哪些没有选择的。下面是他的一个代码展示:
unSelected() {
let result = this.saleList.filter((item) => {
return (
// 这里用evey代表的是,必须spu中的所有元素跟item的name不一样,才会返回true
this.spu.spuSaleAttrList.every((item1) => {
return item.name != item1.saleAttrName;
})
);
});
return result;
},
- 在这个功能区中涉及到的细节功能很多,比如说是图片上传之前处理图片数组添加属性,上传之后获取新的图片列表,下拉框中选中某个属性之后,如何获取到这个属性名以及属性id,在销售属性的table表格中,也同样涉及到了一些功能点。在功能区部分,我会集中说明一下,这块的一些功能是如何完成的。
3. 修改spu页面展示:
这部分页面用的是跟添加一个页面区,只不过的是在进入这个页面去的时候,就会得到数据,并进行展示。
4. 添加sku页面展示:
在这个页面中整体就是一个el-form表单包裹起来的,涉及到的下拉菜单,图片的展示,这些功能前几个页面也都有涉及,主要就是数据的收集,这里涉及到了好几个列表。
4. 查看当前spu的全部sku列表
由一个大的table标签组成的。
页面代码的展示:
<el-dialog
:title="`${spu.spuName}的sku列表`"
:visible.sync="dialogVisible"
width="70%"
:before-close="handleClose"
>
<el-table :data="skuList" style="width: 100%" v-loading="loading">
<el-table-column prop="skuName" label="名称" width="width">
</el-table-column>
<el-table-column prop="price" label="价格" width="width">
</el-table-column>
<el-table-column prop="weight" label="重量" width="width">
</el-table-column>
<el-table-column prop="prop" label="默认图片" width="width">
<template slot-scope="{ row, $index }">
<el-image
style="width: 100px; height: 100px"
:src="row.skuDefaultImg"
></el-image>
</template>
</el-table-column>
</el-table>
</el-dialog>
注意:el-table身上的v-loading="loading",表示的是表格数据的懒加载,在数据还没有回来的时候
6. 当前spu的全部sku列表展示:
<el-table :data="skuList" style="width: 100%" v-loading="loading">
<el-table-column prop="skuName" label="名称" width="width">
</el-table-column>
<el-table-column prop="price" label="价格" width="width">
</el-table-column>
<el-table-column prop="weight" label="重量" width="width">
</el-table-column>
<el-table-column prop="prop" label="默认图片" width="width">
<template slot-scope="{ row, $index }">
<el-image
style="width: 100px; height: 100px"
:src="row.skuDefaultImg"
></el-image>
</template>
</el-table-column>
</el-table>
2. 页面中涉及到的功能点
2.1 首页场景的切换
这里定义了scene属性,用来控制哪个功能区展示。其中scene=0是主页面展示;scene=1是添加或修改spu,这里用的是v-show,不用v-if,因为子组件并没有卸载,只是显示与隐藏。这个是spu组件的显示代码: <spuForm v-show="scene == 1" @changeScene="changeScene" ref="spu" />
// spuForm自定义事件回调,在子组件中点击保存或关闭的时候触发。
changeScene({ scene, flag }) {
// flag这个形参是为了区分保存按钮时添加还是修改
this.scene = scene;
// 传入当前页
if (flag == "修改") {
this.getSpuList(this.page);
} else {
this.getSpuList();
}
},
2.2 添加spu以及修改spu页面的一些功能
2.2.1添加/修改spu页面的数据获取:
说明:子组件要向服务器获取存储的品牌的信息、图片数据、品牌的销售属性,下面是修改initSpuData的代码展示:
async initSpuData(row) {
// 获取spu信息
let result = await this.$API.spu.reqSpu(row.id);
if (result.code == 200) {
this.spu = result.data;
}
// 获取品牌的信息,不需要传入属性
let tradeMarkResult = await this.$API.spu.reqTradeMarkList();
if (tradeMarkResult.code == 200) {
this.tradeMarkList = tradeMarkResult.data;
}
// 获取SPU图片的数据,需要传入id
let spuImageResult = await this.$API.spu.reqSpuImageList(row.id);
if (spuImageResult.code == 200) {
// 因为照片墙显示图片的数据,需要的是数组,数组里面的元素需要name和url字段
// 需要把服务器返回的数据进行修改,因为照片墙里需要的字段是img还有url,但是服务器返回的不是这个样子
let ImageList = spuImageResult.data;
ImageList.forEach((element) => {
element.name = element.imgName;
element.url = element.imgUrl;
});
this.spuImageList = ImageList;
}
// 获取平台销售属性
let saleResult = await this.$API.spu.reqBaseSaleAttrList();
if (saleResult.code == 200) {
this.saleList = saleResult.data;
}
},
注意:表单收集到了data中的spu里面,初始化的时候,不能就只是一个spu:{},这样写是不行的,数据需要确定收集到哪些字段上面,因此需要在创建的时候就明确。
2.2.2:销售属性收集
销售属性的name和id的收集,他这里是新定义了一个新的变量去接受这个字符串。值是el-option身上的value属性,用了模板字符串,然后值传递到了el-select身上的v-model绑定的attrId上。
<el-select
:placeholder="`还有${unSelected.length}未选择`"
v-model="attrId"
>
<el-option
:label="unselect.name"
:value="`${unselect.id}:${unselect.name}`"
v-for="(unselect, index) in unSelected"
:key="unselect.id"
></el-option>
</el-select>
<el-button
type="primary"
icon="el-icon-plus"
:disabled="!attrId"
@click="addSaleAttr"
>添加销售属性</el-button
>
addSaleAttr的方法的代码展示:通过split对字符串进行切割,然后将获得到的数据,加入到新创建的newSaleAttr里面,这个对象是为了方便接下来的销售值名称列表的收集。最后再清空一下attrId,方便下次再次获取。
// 添加销售属性
addSaleAttr() {
// 把手机到的销售属性按钮进行分割
const [baseSaleAttrId, saleAttrName] = this.attrId.split(":");
// 向spu对象里面进行添加
let newsSaleAttr = {
baseSaleAttrId,
saleAttrName,
spuSaleAttrValueList: [],
};
this.spu.spuSaleAttrList.push(newsSaleAttr);
// 清空数据
this.attrId = "";
},
2.2.3:属性名称列表的添加删除功能
<el-button
v-else
class="button-new-tag"
size="small"
@click="addSaleAttrValue(row)"
>添加</el-button
>
addSaleAttrValue方法的代码,this.$set方法可以给对象身上添加响应式数据,inputVlisible用于给spuSaleAttrList列表中的输入框、按钮,判断这俩谁显示谁不显示,inputValue则是接受输入的销售属性的值的。**首先为啥不定义一个全局变量,非要在对象身上添加呢?**如果是全局的话,那么点击只有,销售属性的每一行的input框都会显示了,这个只能是特定行的那个input,要控制这个的话,就必须是给对象身上添加响应式属性。
addSaleAttrValue(row) {
// 用set给item身上添加响应式属性flag,属性值为false
this.$set(row, "inputVisible", true);
this.$set(row, "inputValue", "");
this.$nextTick((_) => {
this.$refs.saveTagInput.focus();
});
},
input框,在输入完成回车之后触发方法handleInputConfirm。这个方法做了三件事:
①判断输入的属性值是否为空
②判断属性值是否重复,用的som
③将获得的属性值加入到spuSaleAttrValueList中
④inputVisible设置为false
// tag自定义标签
handleInputConfirm(row) {
const { baseSaleAttrId, inputValue } = row;
// 新增的属性值不能为空
if (inputValue.trim() == "") {
this.$message("属性值不能为空");
return;
}
// 属性值不能重复
let result = row.spuSaleAttrValueList.some((item) => {
return item.saleAttrValueName == inputValue;
});
if (result) {
this.$message("属性值重复");
return;
}
// 新增的销售属性值
let newSaleAttrValue = { baseSaleAttrId, saleAttrValueName: inputValue };
row.spuSaleAttrValueList.push(newSaleAttrValue);
row.inputVisible = false;
},
删除输入的属性值:
@close="row.spuSaleAttrValueList.splice(index, 1)"
2.2.4 保存spu
addOrUpdateSpu的代码如下所示:这个方法做了三件事
①整理参数,因为图片列表之前为了展出出来。加了俩属性在里面,这个呢,就用到了map方法,返回只包含imageName以及imageUrl属性值的图像列表。
②发送请求,将输入给服务器传过去。
③通知父组件返回到场景0。
// 保存按钮的回调
async addOrUpdateSpu() {
// 整理参数
// 携带参数,这里是直接赋值
this.spu.spuImageList = this.spuImageList.map((item) => {
return {
imageName: item.name,
//这里是因为照片墙里有的数据是从服务器那边返回回来的,因此需要处理一下
imageUrl: (item.response && item.response.data) || item.url,
};
});
// 发请求
let result = await this.$API.spu.reqAddorUpdateSpu(this.spu);
if (result.code == 200) {
this.$message({ type: "success", message: "修改成功" });
// 通知父组件返回到场景0
this.$emit("changeScene", {
scene: 0,
flag: this.spu.id ? "修改" : "添加",
});
}
Object.assign(this._data, this.$options.data());
},
添加spu/修改spu的功能页面完成。
2.3 添加sku属性
2.3.2 获取数据:
用户点击添加sku按钮,父组件会将一些属性传递给子组件通过绑定在子组件身上的ref获得子组件身上的getData方法。
下面是getData方法的代码展示:
async getData(category1Id, category2Id, category3Id, spu) {
// 将父组件给的数据赋值给skuInfo
this.skuInfo.category3Id = category3Id;
// 收集父组件给与的数据
this.skuInfo.spuId = spu.id;
this.skuInfo.tmId = spu.tmId;
this.spu = spu;
// 获取图片数据
let result = await this.$API.sku.reqSpuImageList(spu.id);
if (result.code == 200) {
this.spuImageList = result.data;
// 给每个图片添加一个属性isdefalut
this.spuImageList.forEach((item) => {
this.$set(item, "isDefault", 0);
});
}
// 获取销售属性
let saleattrListresult = await this.$API.sku.reqSpuSaleAttrList(spu.id);
if (saleattrListresult.code == 200) {
this.SpuSaleAttrList = saleattrListresult.data;
}
// 获取平台属性的数据
let attrInfoListresult = await this.$API.sku.reqAttrInfoList(
category1Id,
category2Id,
category3Id
);
if (attrInfoListresult.code == 200) {
this.attrInfoList = attrInfoListresult.data;
}
},
2.3.3 获得平台属性以及销售属性
这一部分的功能跟spu功能页中销售属性的获取差不多,就是用到了el-select标签,然后再el-option用for循环遍历,然后再用模板字符串获取销售属性的id以及name,其中平台属性中v-model绑定的是attrInfoList.attrIdandValueId上面,销售属性也是一样。
2.3.4 获取选中的图片列表
这里涉及需要收集到skuInfo身上的两个变量,skuDefaultImg(默认图片)以及skuImageList(从用户选中的图片列表中收集)
图片列表绑定的事件:@selection-change="handleSelectionChange"
// table表格复选按钮的事件
handleSelectionChange(val) {
// 获取到用户选中图片的信息数据,但是需要注意的是,当前收集的数据,缺少skuDefaultImg
this.imageList = val;
},
注意:这里有一点是需要注意的,就是在收集图片列表的时候,又重新定义了一个数组来去收集,而不能在原来数组的基础上去收集,因为原来数组需要在页面进行展示,不能修改。
收集skuDefaultImg时,需要点击图片右边的按钮,且只能有一个默认图片,点击只有,按钮样式会切换。这里用到了排他思想:
changeDdefault(row) {
// 排他方法
this.spuImageList.forEach((item) => {
item.isDefault = 0;
});
row.isDefault = 1;
this.skuInfo.skuDefaultImg = row.imgUrl;
},
2.3.5 获取已经收集好了的表单进行保存提交
下面是save保存的代码展示:
他主要完成了一下几个功能:
①将收集到的平台属性进行处理,因为之前将id还有name组合成了一个字符串,这里就通过split把他们分开了,然后定义了一个对象,push到了attrInfoList里面。
②将整理好的attrInfoList赋值给skuInfo中的skuAttrValueList 。
③整理销售属性,同样跟平台属性的处理是一样的。
④整理图片数据
⑤发请求,将收集好的表单元素传给服务器
⑥想父组件发请求,执行自定义方法changeScene2,切换视图
⑦清空data中的数据,将其中赋值了的值空,这里写的很巧妙:Object.assign(this._data, this.$options.data());
async save() {
let arr = [];
this.attrInfoList.forEach((item) => {
if (item.attrIdandValueId) {
const [attrId, valueId] = item.attrIdandValueId.split(":");
let obj = { attrId, valueId };
arr.push(obj);
}
});
// 将整理好的参数传递给
this.skuInfo.skuAttrValueList = arr;
// 整理销售属性
this.skuInfo.skuSaleAttrValueList = this.SpuSaleAttrList.reduce(
(prev, item) => {
if (item.saleattrIdandValueId) {
const [
saleAttrId,
saleAttrValueId,
] = item.saleattrIdandValueId.split(":");
prev.push({ saleAttrId, saleAttrValueId });
}
return prev;
},
[]
);
// 下面就剩下图片了
this.skuInfo.skuImageList = this.imageList.map((item) => {
return {
imgName: item.imgName,
imgUrl: item.imgUrl,
isDefault: item.isDefault,
spuImgId: item.id,
};
});
// 发请求
let result = await this.$API.sku.reqAddSku(this.skuInfo);
if (result.code == 200) {
this.$message({ type: "success", message: "保存成功" });
this.$emit("changeScene2", 0);
Object.assign(this._data, this.$options.data());
}
},
2.4 首页按钮的封装,使其可以在鼠标放到这个按钮上的时候,显示提示信息。
下面是封装好的组件内的代码展示:
<template>
<a :title="title" style="margin: 10px">
<!-- 使用 v-bind="$attrs" 属性,vm.$attrs 是一个属性,其包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。
这些未识别的属性可以通过 v-bind="$attrs" 传入内部组件。未识别的事件可通过v-on="$listeners"传入。 -->
<el-button v-bind="$attrs" v-on="$listeners"></el-button>
</a>
</template>
3. sku管理页面
1. 页面展示
2. 查看详情页面
点击操作按钮中的查看详情弹出来的
这里的轮播图用到的标签组件是el-carousel,下面是他的代码部分:
<!-- 轮播图 -->
<el-carousel height="150">
<el-carousel-item
v-for="(item, index) in skuInfo.skuImageList"
:key="item.id"
>
<el-image
:src="item.imgUrl"
style="width=100%;height=150px "
></el-image>
<!-- <h3>{{ item.imgUrl }}</h3> -->
</el-carousel-item>
</el-carousel>
总结:
在写这个项目的时候,要格外的注意,服务器需要什么样的数据,如何去获取到,每个组件标签都跟哪些数据进行绑定的,有哪些数据需要传递到服务器的,父组件如何控制子组件显示与隐藏,还有就是数据在从组件标签身上获取的时候,需要进行哪些操作,什么数据应该写在computed什么数据应该写在watch中,这些东西在写前端页面逻辑的时候较为重要。