part1
part2
part3
part4
part5
part6(本页)
7. 菜品展示、购物车、下单 功能开发
7.1 导入用户地址簿相关功能代码
7.2 菜品展示
7.3 购物车
7.4 下单
7.1 导入用户地址簿相关功能代码
7.1.1 整体分析
- 需求分析
- 数据模型
- 需要开发的模块:新增收获地址、设置默认地址
7.1.2 前端代码分析
- 把addressList(vue 中的data模块)中的数据展示到页面上代码
<div class="divContent">
<div class="divItem" v-for="(item,index) in addressList" :key="index" @click.capture="itemClick(item)">
<div class="divAddress">
<span :class="{spanCompany:item.label === '公司',spanHome:item.label === '家',spanSchool:item.label === '学校'}">{{item.label}}</span>
<!--把addressList中的数据循环出来-->
{{item.detail}}
</div>
<div class="divUserPhone">
<span>{{item.consignee}}</span>
<span>{{item.sex === '0' ? '女士' : '先生'}}</span>
<span>{{item.phone}}</span>
</div>
<!--
@click.stop.prevent 是 Vue.js 中的指令,用于阻止事件的默认行为和事件冒泡。
当该指令绑定在一个元素上时,当该元素被点击时,
会阻止事件的默认行为(如链接跳转)和事件冒泡(即不会向父元素传递事件)。
toAddressEditPage,点击后跳转到对应的页面,编辑,后面的添加也一样
-->
<img src="./../images/edit.png" @click.stop.prevent="toAddressEditPage(item)"/>
<div class="divSplit"></div>
<div class="divDefault" >
<!--就是默认地址选中了没有-->
<img src="./../images/checked_true.png" v-if="item.isDefault === 1">
<img src="./../images/checked_false.png" @click.stop.prevent="setDefaultAddress(item)" v-else>设为默认地址
</div>
</div>
</div>
<!--页面底部显示的-->
<div class="divBottom" @click="toAddressCreatePage">+ 添加收货地址</div>
</div>
页面效果如下:
点击添加收获地址之后触发函数发送跳转,类似的编辑也是这样
toAddressCreatePage(){
window.requestAnimationFrame(()=>{
window.location.href= '/front/page/address-edit.html'
})
},
- 编辑 address-edit.html
页面效果
钩子函数created() 执行之后,执行了initData(),这里面有一个根据id查询地址的功能,从数据库查询到之后 ,显示到页面上addressFindOneApi;
async initData(){
/*
window.location.search 是 JavaScript 中的一个属性,
它表示当前页面的 URL 中的查询字符串部分。
例如,如果当前页面的 URL 是 "https://www.example.com/search?q=javascript&page=2",
那么 window.location.search 的值就是 "?q=javascript&page=2"。
* */
const params = parseUrl(window.location.search)
this.id = params.id
if(params.id){
this.title = '编辑收货地址'
const res = await addressFindOneApi(params.id)
if(res.code === 1){
this.form = res.data
}else{
this.$notify({ type:'warning', message:res.msg});
}
}
}
增加和删除的前端代码,还是很好理解的.
async saveAddress(){
const form = this.form
if(!form.consignee){
this.$notify({ type:'warning', message:'请输入联系人'});
return
}
if(!form.phone){
this.$notify({ type:'warning', message:'请输入手机号'});
return
}
if(!form.detail){
this.$notify({ type:'warning', message:'请输入收货地址'});
return
}
const reg = /^1[3|4|5|7|8][0-9]{9}$/
if(!reg.test(form.phone)){
this.$notify({ type:'warning', message:'手机号码不合法'});
return
}
let res= {}
if(this.id){
res = await updateAddressApi(this.form)
}else{
res = await addAddressApi(this.form)
}
if(res.code === 1){
window.requestAnimationFrame(()=>{
window.location.replace('/front/page/address.html')
})
}else{
this.$notify({ type:'warning', message:res.msg});
}
},
deleteAddress(){
this.$dialog.confirm({
title: '确认删除',
message: '确认要删除当前地址吗?',
})
.then( async () => {
const res = await deleteAddressApi({ids:this.id })
if(res.code === 1){
window.requestAnimationFrame(()=>{
window.location.replace('/front/page/address.html')
})
}else{
this.$notify({ type:'warning', message:res.msg});
}
})
.catch(() => {
});
},
其中三个关键的axios请求:
//新增地址
function addAddressApi(data){
return $axios({
'url': '/addressBook',
'method': 'post',
data
})
}
//修改地址
function updateAddressApi(data){
return $axios({
'url': '/addressBook',
'method': 'put',
data
})
}
//删除地址
function deleteAddressApi(params) {
return $axios({
'url': '/addressBook',
'method': 'delete',
params
})
}
7.1.3 后端代码分析
这部分比较简单,就是接收前端的数据,从前面看到,提交的都是大多json格式的数据,需要使用@RequestBody 获取。
- 页面根据id查询地址,显示出来。
前端请求是这样的
function addressFindOneApi(id) {
return $axios({
'url': `/addressBook/${id}`,
'method': 'get',
})
}
controller:
@GetMapping("/{id}")
public RetObj get(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);
if (addressBook != null) {
return RetObj.success(addressBook);
} else {
return RetObj.error("没有找到该对象");
}
}
- 把所有地址显示到页面上
前端:
async initData(){
const res = await addressListApi()
if(res.code === 1){
this.addressList = res.data
}else{
this.$message.error(res.msg)
}
},
后端:
@GetMapping("/list")
public RetObj<List> getAddressById(){
Long userId = BaseContext.getThreadLocal();
LambdaQueryWrapper<AddressBook> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(AddressBook::getUserId,userId)
.orderByDesc(AddressBook::getUpdateTime);
//SQL:select * from address_book where user_id = ? order by update_time desc
List<AddressBook> addressBooks = addressBookService.list(lambdaQueryWrapper);
return RetObj.success(addressBooks);
}
- 新增
@PostMapping("")
public RetObj<String> addAddressController(@RequestBody AddressBook addressBook){
//注意需要 将当前操作插入的用户注入,使用ThreadLocal
addressBook.setUserId(BaseContext.getThreadLocal());
boolean res = addressBookService.save(addressBook);
if (res){
return RetObj.success("成功新增地址");
}else {
return RetObj.error("地址添加失败!");
}
}
- 设置默认地址
前端,点击按钮,设置为默认,注意参数是json
async setDefaultAddress(item){
if(item.id){
const res = await setDefaultAddressApi({id:item.id})
if(res.code === 1){
this.initData()
}else{
this.$message.error(res.msg)
}
}
},
后端,根据id 设置默认地址,有很多注意的点,比如先得把表中所有地址设置为非默认,否则就有两个默认地址
/**
* 设置默认的地址,需要把其他地址都先设置为非默认的!!(而且是当前user对应的地址)
* @param addressBook
* @return
*/
@PutMapping("/default")
public RetObj setDefaultAddress(@RequestBody AddressBook addressBook){
log.info("addressBook:{}", addressBook);
LambdaUpdateWrapper<AddressBook> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
lambdaUpdateWrapper.eq(addressBook != null,AddressBook::getUserId
,BaseContext.getThreadLocal())
.set(AddressBook::getIsDefault,0);
boolean res = addressBookService.update(lambdaUpdateWrapper);
//上面先把所有的地址都设置为非默认
//现在把指定的地址设置为默认
addressBookService.updateById(addressBook);//会根据非null的字段进行更新!!
return RetObj.success("成功设置为默认地址");
}
- 查询默认地址(一开始把所有地址显示出来之后,要查一下哪个地址是默认的,然后在页面显示,对应地址是默认地址)
/**
* 查询默认地址,因为可能没有查到地址等情况,所以返回值需要判别一下
* SQL:select * from address_book where user_id = ? and is_default = 1
*/
@GetMapping("default")
public RetObj<AddressBook> getDefault() {
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getUserId, BaseContext.getThreadLocal());
queryWrapper.eq(AddressBook::getIsDefault, 1);
//SQL:select * from address_book where user_id = ? and is_default = 1
AddressBook addressBook = addressBookService.getOne(queryWrapper);
if (null == addressBook) {
return RetObj.error("没有找到该对象");
} else {
return RetObj.success(addressBook);
}
}
7.2 移动端菜品展示
7.2.1 整体分析
- 需求分析
2.交互过程分析
分类数据展示到左边的菜单栏上,在之前的分类管理中,其实已经写好了 (就是之前,选中菜品的分类的时候,下拉框就需要显示有哪些分类,正好现在直接复用) ;
一打开页面也要默认展示第一个分类下的菜品,不要点击了才展示出来(比如现在的湘菜)。类似的,代码之前也写的差不多了,之前在新增分类中,选择该分类下的菜品(下拉框中)。但是之前返回值是List<Dish>
,现在不仅仅需要Dish,还需要Dish对应的口味信息,后端那边要对这个进行改造(使用DishDto)!
7.2.2 前端代码分析
- 显示分类,循环读取categoryList的内容。如果点击了对应的套餐,显示套餐下的菜品,展示到右边
<div class="divType">
<ul>
<li v-for="(item,index) in categoryList" :key="index" @click="categoryClick(index,item.id,item.type)" :class="{active:activeType === index}">{{item.name}}</li>
</ul>
</div>
显示出套餐下的菜品,调用getDishList()
//分类点击
categoryClick(index,id,type){
this.activeType = index
this.categoryId = id
if(type === 1){//菜品
this.getDishList()
}else{
this.getSetmealData()
}
},
getDishList()具体方法如下,获取了后端分装好的data数据,保存在dishList中进行数据的双向绑定。
//获取菜品数据
async getDishList(){
if(!this.categoryId){
return
}
const res = await dishListApi({categoryId:this.categoryId,status:1})
if(res.code === 1){
let dishList = res.data
const cartData = this.cartData
if(dishList.length > 0 && cartData.length > 0){
dishList.forEach(dish=>{ //循环为DishList插入cart值
cartData.forEach(cart=>{
if(dish.id === cart.dishId){
dish.number = cart.number
})
}
this.dishList = dishList
}else{
this.$notify({ type:'warning', message:res.msg});
}
},
具体发送的axios请求。
function dishListApi(data) {
return $axios({
'url': '/dish/list',
'method': 'get',
params:{...data}
})
}
- 接着上面,点击对应的菜品分类后(数据已经存到dishList中),现在要循环显示出该分类下的菜品,并且能够对菜品进行操作,包括显示月销,选择规格等。同时,如果点击进去,会显示具体的细节,对应@click="dishDetails(item)
<div class="divMenu">
<div>
<div class="divItem" v-for="(item,index) in dishList" :key="index" @click="dishDetails(item)">
<el-image :src="imgPathConvert(item.image)" >
<div slot="error" class="image-slot">
<img src="./images/noImg.png"/>
</div>
</el-image>
<div>
<div class="divName">{{item.name}}</div>
<div class="divDesc">{{item.description}}</div>
<div class="divDesc">{{'月销' + (item.saleNum ? item.saleNum : 0) }}</div>
<div class="divBottom"><span>¥</span><span>{{item.price/100}}</span></div>
<div class="divNum">
<div class="divSubtract" v-if="item.number > 0">
<img src="./images/subtract.png" @click.prevent.stop="subtractCart(item)"/>
</div>
<div class="divDishNum">{{item.number}}</div>
<div class="divTypes" v-if="item.flavors && item.flavors.length > 0 && !item.number " @click.prevent.stop="chooseFlavorClick(item)">选择规格</div>
<div class="divAdd" v-else>
<img src="./images/add.png" @click.prevent.stop="addCart(item)"/>
</div>
</div>
</div>
</div>
</div>
进行了后端交互,调用setMealDishDetailsApi()
async dishDetails(item){
//先清除对象数据,如果不行的话dialog使用v-if
this.detailsDialog.item = {}
this.setMealDialog.item = {}
if(Array.isArray(item.flavors)){
this.detailsDialog.item = item
this.detailsDialog.show = true
}else{
//显示套餐的数据
const res = await setMealDishDetailsApi(item.id)
if(res.code === 1){
this.setMealDialog.item = {...item,list:res.data}
this.setMealDialog.show = true
}else{
this.$notify({ type:'warning', message:res.msg});
}
}
}
axios交互:
//获取套餐的全部菜品
function setMealDishDetailsApi(id) {
return $axios({
'url': `/setmeal/dish/${id}`,
'method': 'get',
})
}
- 初始化数据,一进来不点击任何东西应该也要显示出对应第一个分类的菜品.
注意Promise.all必须是购物车和分类都加载完毕了,才把数据初始化到界面上。只有这里面所有的请求都完成了,才会成功执行接下来的代码,才会把categoryList等等数据提交成功。所以没写购物车逻辑的时候,就算分类写好了也显示不出来
mounted(){
this.initData()
},
methods:{
//初始化数据
initData(){
Promise.all([categoryListApi(),cartListApi({})]).then(res=>{
//获取分类数据
if(res[0].code === 1){
this.categoryList = res[0].data
if(Array.isArray(res[0].data) && res[0].data.length > 0){
this.categoryId = res[0].data[0].id
if(res[0].data[0].type === 1){
this.getDishList()
}else{
this.getSetmealData()
}
}
}else{
this.$notify({ type:'warning', message:res[0].msg});
}
//获取菜品数据
if(res[1].code === 1){
this.cartData = res[1].data
}else
this.$notify({ type:'warning', message:res[1].msg});
}
})
},
- 上面的代码完成所有分类和套餐的名字的展示,没有把具体的套餐和菜品的信息显示出来,上面代码中调用了getDishList()方法,具体如下(对应type === 1)
//获取菜品数据
async getDishList(){
if(!this.categoryId){
return
}
const res = await dishListApi({categoryId:this.categoryId,status:1})
if(res.code === 1){
let dishList = res.data
const cartData = this.cartData
if(dishList.length > 0 && cartData.length > 0){
dishList.forEach(dish=>{ //为每一条dishList插入cart值
cartData.forEach(cart=>{
if(dish.id === cart.dishId){
dish.number = cart.number
}
})
})
}
this.dishList = dishList
}else{
this.$notify({ type:'warning', message:res.msg});
}
},
对应api:
function dishListApi(data) {
return $axios({
'url': '/backend/page/food/list/getDishByCategoryId.do',
'method': 'get',
params:{...data}
})
}
- 现在type不是1,那就是要展示套餐的具体信息:getSetmealData(),和上面很类似
//获取套餐数据setmealId
async getSetmealData(){
if(!this.categoryId){
return
}
const res = await setmealListApi({categoryId:this.categoryId,status:1})
if(res.code === 1){
let dishList = res.data
const cartData = this.cartData
if(dishList.length > 0 && cartData.length > 0){
dishList.forEach(dish=>{
cartData.forEach(cart=>{
if(dish.id === cart.setmealId){
dish.number = cart.number
}
})
})
}
this.dishList = dishList
}else{
this.$notify({ type:'warning', message:res.msg});
}
},
setmealListApi如下
function setmealListApi(data) {
return $axios({
'url': '/setmeal/list',
'method': 'get',
params:{...data}
})
}
- 以上完成的是具体菜品或者套餐在页面右边的展示,现在如果点击了 具体菜品或者套餐(如:麻辣兔头、二逼套餐A计划),要显示该菜品的具体信息。
async dishDetails(item){
//先清除对象数据,如果不行的话dialog使用v-if
this.detailsDialog.item = {}
this.setMealDialog.item = {}
if(Array.isArray(item.flavors)){
this.detailsDialog.item = item
this.detailsDialog.show = true
}else{
//显示套餐的数据
const res = await setMealDishDetailsApi(item.id)
if(res.code === 1){
this.setMealDialog.item = {...item,list:res.data}
this.setMealDialog.show = true
}else{
this.$notify({ type:'warning', message:res.msg});
}
}
}
7.2.3 后端分析
- 查出所有套餐、菜品的分类,显示出来(左边的栏目)。之前写好了,直接复用,注意加上category.getType() != null。
@GetMapping("/food/list/getCategory.do")
public RetObj getCategoryList(Category category){
LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//category.getType() != null 加上了这个,因为在移动端front中会直接查询,不带type,把菜品和套餐全部查出来
lambdaQueryWrapper.eq(category.getType() != null,Category::getType,category.getType())
.orderByAsc(Category::getSort)
.orderByDesc(Category::getUpdateTime);
List<Category> categoryList = categoryService.list(lambdaQueryWrapper);
//log.info("查询出菜品:{}",categoryList);
return RetObj.success(categoryList);
}
- 点击左边的菜品分类后,查出该菜品分类对应有哪些菜。如川菜有:麻辣兔头等。
@GetMapping("list/getDishByCategoryId.do")
public RetObj getDishByCategoryId(Dish dish){
LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(dish != null,Dish::getCategoryId,dish.getCategoryId())
.orderByDesc(Dish::getSort);
List<Dish> dishList = dishService.list(lambdaQueryWrapper);
return RetObj.success(dishList);
}
- 点击套餐分类后弹出该套餐分类下有哪些套餐。(setmeal套餐表中有category_id字段,就根据这个字段进行查询即可(左边栏目中点击的就传category_id,如点击二逼套餐,传递二逼套餐对应的category_id,查出二逼套餐A计划),后端开发需要注意state==1,没有被禁用的才能查出。)
如儿童套餐里面有 儿童套餐A计划、儿童套餐B计划等。每个具体套餐又含有对应的菜品。如二逼套餐中有二逼套餐A计划,二逼套餐A计划中有二逼(二逼这个具体的菜又属于二逼菜这个菜品分类)。 如下图添加套餐管理时就是这样添加的。
/**
* 查出套餐分类下有哪些套餐:如二逼套餐中有二逼套餐A计划、B计划等(二逼套餐A计划中有二逼(二逼这个具体的菜又属于二逼菜这个菜品分类)
* 注意,
* @param 主要就是categoryID,还有state是1,没被禁用的查出
* @return
*/
@GetMapping("list/getSetMealByCategoryId.do")
public RetObj getSetMealByCategoryId(Long categoryId,Integer status){
LambdaQueryWrapper<Setmeal> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Setmeal::getCategoryId,categoryId)
.eq(Setmeal::getStatus,status)
.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> setmealList = setmealService.list(lambdaQueryWrapper);
return RetObj.success(setmealList);
}
做完后效果如下
4. 以上是完成左边分类栏目的显示,以及点击分类栏目在右边示出该分类具体的项目,现在点击具体的项目,要显示出详情。比如点击上面的二逼套餐A计划,要能看到这个套餐中有哪些菜品。
- 这部分一定要搞清楚表的结构! 举例:二逼套餐点击后有二逼套餐A计划,现在就是想点击二逼套餐A计划,看有哪些菜。
-
- 二逼套餐A计划 表中有id是自己的id,有categoryId是二逼套餐的Id。
-
- 在setmeal_dish表中有 setmeal_id、dish_id、id 现在这些id都很清楚明白了
-
- 用二逼套餐A计划自己的Id(主键)去查setmeal_dish中的setmeal_id(副键)
/**
* 获取套餐的全部菜品
* @param id 注意这个id是二逼套餐A计划套餐的id,查表setmeal_dish中的setmeal_id这个字段对应起来
* @return
*/
@GetMapping("setmeal/setMealDishDetails.do/{id}")
public RetObj setMealDishDetails(@PathVariable Long id){
LambdaQueryWrapper<SetmealDish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.eq(SetmealDish::getSetmealId,id)
.orderByDesc(SetmealDish::getUpdateTime);
List<SetmealDish> setmealDishes = setmealDishService.list(dishLambdaQueryWrapper);
return RetObj.success(setmealDishes);
}