【易售小程序项目】小程序首页完善(滑到底部数据翻页、回到顶端、基于回溯算法的两列数据高宽比平衡)【后端基于若依管理系统开发】

news2025/1/22 12:26:44

文章目录

  • 说明
  • 细节一:首页滑动到底部,需要查询下一页的商品
    • 界面预览
    • 页面实现
  • 细节二:当页面滑动到下方,出现一个回到顶端的悬浮按钮
  • 细节三:商品分列
    • 说明
    • 优化前后效果对比
    • 使用回溯算法实现
      • Controller
      • Service
      • 回溯算法
    • 优化:减少图片的网络请求
      • 数据表增加字段
      • 将数据表中已有数据的宽高比计算出来,并更新到数据表中
      • 修改商品发布页面的代码
      • Service改进
    • 优化:考虑分页的分组高宽比总和平衡
      • 页面代码
      • Controller
      • Service
      • 回溯算法
    • 优化:考虑商品信息的高宽比
      • Controller
      • Service
      • 回溯算法
  • 页面整体代码

说明

之前已经在【UniApp开发小程序】小程序首页(展示商品、商品搜索、商品分类搜索)【后端基于若依管理系统开发】这篇文章中介绍了首页的实现,但是当时的实现并不是完善的,因为项目开发是一个持续的过程,也因为我是个人的第一次尝试开发这种类型的项目,很多细节没有提前构思清楚,因此这篇文章作为一个补充,用来优化前面的一些问题

细节一:首页滑动到底部,需要查询下一页的商品

界面预览

当滑动底部的时候,底部出现”正在加载“字样,同时向后端发送请求获取下一页的商品数据

在这里插入图片描述
当商品被全部加载出之后,显示“没有更多了”字样

在这里插入图片描述

页面实现

下面的方法可以监听用户滑动页面到达底部,当滑动到底部的时候,调用方法查询更多商品

// 监听用户滑动到底部
onReachBottom() {
	this.getMoreProductVo();
},

注意,当还有商品没有被查询出来时,才会调用listProductVo方法去找服务端查询数据。如果没有了,则提示“没有更多了”

/**
 * 获取下一页的商品
 */
getMoreProductVo() {
	if (this.productList[0].length + this.productList[1].length >= this.total) {
		// this.$refs.uToast.show({
		// 	type: 'warning',
		// 	message: "已经加载完所有商品数据",
		// 	duration: 1000
		// })
	} else {
		if (this.loadData != true) {
			// console.log("--------------------------获取下一页商品---------------------------")
			this.page.pageNum++;
			// 显示正在加载
			this.loadmoreStatus = "loading";
			this.listProductVo().then(() => {
				if (this.productList[0].length + this.productList[1].length >= this.total) {
					// 没有更多了
					this.loadmoreStatus = "nomore";
				} else {
					// 加载更多
					this.loadmoreStatus = "loadmore";
				}
			});
		}
	}
},

细节二:当页面滑动到下方,出现一个回到顶端的悬浮按钮

增加一个标签

<!-- 回到上方按钮 -->
<u-back-top :scroll-top="scrollTop"></u-back-top>

因为标签绑定了一个变量,需要声明出来

// 用来控制滚动到最上方
scrollTop: 0

除此之外,还需要实时记录滚动的位置

// 在滑动过程实时获取现在的滚动条位置,并保存当前的滚动条位置
onPageScroll(e) {
	this.scrollTop = e.scrollTop;
},

细节三:商品分列

说明

上篇文章中,使用了最简单的方式来实现分列,那就是直接遍历一遍商品数组,依次将商品分到第一列和第二列,但是这样会出现两列商品高度不平衡的情况,如下图
在这里插入图片描述
因此,我们需要更换一种分组策略,用来平衡两列商品内容的高度,这样视觉效果更好。问题可以理解为:假设有十个物品,每个物品的长度不太一样,要求将这些物品分到两组中,最后两组物品长度总和最接近,请问需要怎么来分这两个组?

优化前后效果对比

在这里插入图片描述

使用回溯算法实现

因为采用的是分页查询,而且每次查询出来的数据量并不大,因此可以直接使用回溯算法获取所有的分组情况,最后选择出高度差距最小的分组方案即可

Controller

/**
 * 查询商品Vo列表
 */
@PreAuthorize("@ss.hasPermi('market:product:list')")
@PostMapping("/listProductVo")
@ApiOperation("获取商品列表")
public AjaxResult listProductVo(@RequestBody ProductVo productVo) {
    startPage();
    if (productVo.getProductCategoryId() != null) {
        // --if-- 当分类不为空的时候,只按照分类来搜索
        productVo.setKeyword(null);
    }
    if (productVo.getIsSearchStar() != null && productVo.getIsSearchStar() == true) {
        productVo.setStarPeopleId(getLoginUser().getUserId());
    }
    List<ProductVo> productVoList = productService.selectProductVoList(productVo);
    // 将productVoList分成两组,要求两组的高度之和相差最小
    List<ProductVo>[] groups = productService.splitToTwoGroups(productVoList);
    Map<String, Object> map = new HashMap<>();
    TableDataInfo pageMes = getDataTable(productVoList);
    map.put("pageMes", pageMes);
    map.put("groups", groups);
    return AjaxResult.success(map);
}

Service

@Override
public List<ProductVo>[] splitToTwoGroups(List<ProductVo> productVoList) {
    List<ProductVo>[] resultArr = new List[2];
    for (int i = 0; i < resultArr.length; i++) {
        resultArr[i] = new ArrayList<>();
    }
    /// 数据准备
    // 获取每个图片的高宽比
    Map<Long, Double> idAndRatioMap = new HashMap<>();
    // 存储所有商品的id
    List<Long> idList = new ArrayList<>();
    long start = System.currentTimeMillis();
    for (ProductVo productVo : productVoList) {
        idList.add(productVo.getId());
        if (productVo.getPicList() != null && productVo.getPicList().size() > 0) {
            try {
                BufferedImage sourceImg = ImageIO.read(new URL(productVo.getPicList().get(0)).openStream());
                idAndRatioMap.put(productVo.getId(), sourceImg.getHeight() * 1.0 / sourceImg.getWidth());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else {
            idAndRatioMap.put(productVo.getId(), 0.0);
        }
    }
    System.out.println("分组时间:" + (System.currentTimeMillis() - start) + "ms");

    /// 深度优先遍历,找出所有方案,选择两组高度差距最小的分组方案
    GroupDivide groupDivide = new GroupDivide();
    groupDivide.dfsSearch(idList, 0, new ArrayList<>(), idAndRatioMap);

    /// 最后处理分组
    List<Long> group1 = groupDivide.bestGroup1;
    List<Long> group2 = new ArrayList<>();
    for (Long id : idList) {
        if (group1.indexOf(id) == -1) {
            group2.add(id);
        }
    }
    for (ProductVo productVo : productVoList) {
        if (group1.indexOf(productVo.getId()) != -1) {
            resultArr[0].add(productVo);
        } else {
            resultArr[1].add(productVo);
        }
    }

	return resultArr;
}

由于下面的方法获取每个图片的高宽比都需要进行网络请求,因此速度较慢,因此需要进行优化

BufferedImage sourceImg = ImageIO.read(new URL(productVo.getPicList().get(0)).openStream());
idAndRatioMap.put(productVo.getId(), sourceImg.getHeight() * 1.0 / sourceImg.getWidth());

在这里插入图片描述

回溯算法

为了加速算法的求解,其中使用了减枝策略,不去搜索没有必要搜索的方案

package com.shm.algorithm;

import com.ruoyi.common.utils.clone.CloneUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 首页商品数据分组
 *
 * @Author dam
 * @create 2023/8/30 14:12
 */
public class GroupDivide {
    /**
     * 最小间距
     */
    private double minOffSet = Double.MAX_VALUE;
    /**
     * 存储最好的第一组
     */
    public List<Long> bestGroup1=null;

    public void dfsSearch(List<Long> idList, int begin, List<Long> curGroup1, Map<Long, Double> idAndRatioMap) {
        if (begin == idList.size()) {
            // 递归完成
            return;
        }
        for (int i = begin; i < idList.size(); i++) {
            curGroup1.add(idList.get(i));
            // 计算组1的长度-组2的长度
            double offSet = calculateGroup1DifHeifGroup2Hei(idList, curGroup1, idAndRatioMap);
            if (offSet > minOffSet) {
                // 如果当前差距已经大于最小差距,执行剪枝,因为如果再往第一组增加图片的话,那差距只会更大,没必要再往下搜索了
                // 删除最后一个元素
                curGroup1.remove(curGroup1.size() - 1);
                continue;
            } else if (Math.abs(offSet) < minOffSet) {
                // 找到更小的间距,保存最优解
                minOffSet = Math.abs(offSet);
                bestGroup1 = CloneUtil.arrayListClone(curGroup1);
            }
            dfsSearch(idList, i + 1, curGroup1, idAndRatioMap);
            // 删除最后一个元素
            curGroup1.remove(curGroup1.size() - 1);
        }
    }

    /**
     * 计算第一组的图片的总高度 减去 第二组图片的总高度
     *
     * @param idList
     * @param group1
     * @param idAndRatioMap
     * @return
     */
    private double calculateGroup1DifHeifGroup2Hei(List<Long> idList, List<Long> group1, Map<Long, Double> idAndRatioMap) {
        double sum1 = 0, sum2 = 0;
        for (Long id : idList) {
            if (group1.indexOf(id) == -1) {
                sum2 += idAndRatioMap.get(id);
            }else {
                sum1 += idAndRatioMap.get(id);
            }
        }
        return sum1 - sum2;
    }

}

优化:减少图片的网络请求

因为图片的高宽比是一个不变量,可以将其作为一个属性存储到数据表中,这样只需要查询出来即可,不再需要使用网络请求来获取,但是需要在存储图片到数据表之前获取高宽比,并将该属性进行存储

数据表增加字段

在这里插入图片描述

将数据表中已有数据的宽高比计算出来,并更新到数据表中

因为我的数据表中已经存在了一些图片数据,为了小程序地正确运行,需要对这批数据进行修复,即为每张图片补充高宽比。因为数据表的数据量不大,而且是一次性任务,直接每次修改单条数据即可。如果数据量很大,可以使用多线程和分批批量修改来优化修复速度

@Override
 public void updatePictureSheetSetAspectRatio() {
     Picture picture = new Picture();
     picture.setType(0);
     // 取消使用分页
     clearPage();
     List<Picture> pictureList = pictureMapper.selectPictureList(picture);
     for (Picture picture1 : pictureList) {
         String address = picture1.getAddress();
         try {
             BufferedImage sourceImg = ImageIO.read(new URL(address));
             picture1.setAspectRatio(sourceImg.getHeight() * 1.0 / sourceImg.getWidth());
             pictureMapper.updatePicture(picture1);
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
     }
 }

修改商品发布页面的代码

现在数据表需要保存图片的高宽比,虽然可以直接由服务端在保存图片之前计算高宽比,但是这样还是要发送很多网络请求,影响接口的并发性能,因此建议由客户端来计算高宽比,然后直接上传给服务端,服务端直接将数据保存即可

/**
* 上传闲置商品
*/
uploadSellProduct() {
	// console.log("上传闲置商品picList:" + JSON.stringify(this.picList));
	if (this.product.productCategoryId) {
		if (this.picList.length == 0) {
			this.$refs.uToast.show({
				type: 'error',
				message: "商品图片没有上传成功"
			})
		} else {
			this.setPicAspectRatio().then(() => {
				// console.log("即将上传的商品:" + JSON.stringify(this.product));
				uploadSellProduct(this.product).then(res => {
					this.$refs.uToast.show({
						type: 'success',
						message: "您的商品已经发布到平台"
					})
					setTimeout(() => {
						uni.reLaunch({
							url: "/pages/index/index"
						})
					}, 1000)
				}).catch(error => {
					console.log("error:" + JSON.stringify(error));
					this.$refs.uToast.show({
						type: 'error',
						message: "商品发布失败"
					})
				});
			});

		}
	} else {
		this.$refs.uToast.show({
			type: 'error',
			message: "请选择分类"
		})
	}
},
/**
 * 设置图片的宽高比
 */
setPicAspectRatio() {
	return new Promise((resolve, reject) => {
		this.product.picList = [];
		let promises = [];
		for (let i = 0; i < this.picList.length; i++) {
			let picUrl = this.picList[i];
			promises.push(this.getAspectRatio(picUrl).then((res) => {
				let pic = {
					address: picUrl,
					aspectRatio: res
				}
				this.product.picList.push(pic);
				console.log("当前图片高宽比设置完成");
			}))
		}
		Promise.all(promises).then(() => {
			console.log("所有图片高宽比设置完成,this.product.picList:" + JSON.stringify(this.product
				.picList));
			resolve();
		})
	})
},
/**
 * 获取单个图片的高宽比

 * @param {Object} url
 */
getAspectRatio(url) {
	return new Promise((resolve, reject) => {
		uni.getImageInfo({
			src: url,
			success: function(res) {
				let aspectRatio = res.height / res.width;
				resolve(aspectRatio);
			}
		});
	})
},

注意点:

  • 因为getAspectRatio方法获取图片的高宽比发送网络请求,因此使用Promise来确保高宽比获取成功才resolve
  • 在上传商品之前,需要先设置商品所对应的所有图片的高宽比。如果图片有多张,需要等待所有图片的高宽比都设置完成,本文使用Promise.all(promises)来等待所有图片的高宽比都设置完成,再resolve

Service改进

因为已经将图片的高宽比存储到数据表中,因此不需要再发送网路请求,直接获取属性值即可

@Override
public List<ProductVo>[] splitToTwoGroups(List<ProductVo> productVoList) {
    List<ProductVo>[] resultArr = new List[2];
    for (int i = 0; i < resultArr.length; i++) {
        resultArr[i] = new ArrayList<>();
    }
    /// 数据准备
    // 获取每个图片的高宽比
    Map<Long, Double> idAndRatioMap = new HashMap<>();
    // 存储所有商品的id
    List<Long> idList = new ArrayList<>();
    long start = System.currentTimeMillis();
    for (ProductVo productVo : productVoList) {
        idList.add(productVo.getId());
        if (productVo.getPicList() != null && productVo.getPicList().size() > 0) {
//                try {
//                    BufferedImage sourceImg = ImageIO.read(new URL(productVo.getPicList().get(0)).openStream());
//                    idAndRatioMap.put(productVo.getId(), sourceImg.getHeight() * 1.0 / sourceImg.getWidth());
//                } catch (IOException e) {
//                    throw new RuntimeException(e);
//                }
            idAndRatioMap.put(productVo.getId(), productVo.getPicList().get(0).getAspectRatio());
        } else {
            idAndRatioMap.put(productVo.getId(), 0.0);
        }
    }
    System.out.println("分组时间:" + (System.currentTimeMillis() - start) + "ms");

    /// 深度优先遍历,找出所有方案,选择两组高度差距最小的分组方案
    GroupDivide groupDivide = new GroupDivide();
    groupDivide.dfsSearch(idList, 0, new ArrayList<>(), idAndRatioMap);

    /// 最后处理分组
    List<Long> group1 = groupDivide.bestGroup1;
    List<Long> group2 = new ArrayList<>();
    for (Long id : idList) {
        if (group1.indexOf(id) == -1) {
            group2.add(id);
        }
    }
    for (ProductVo productVo : productVoList) {
        if (group1.indexOf(productVo.getId()) != -1) {
            resultArr[0].add(productVo);
        } else {
            resultArr[1].add(productVo);
        }
    }

    return resultArr;
}

【测试】
在不需要发送网络请求之后,可以看到获取图片高宽比的时间被大大减少

在这里插入图片描述

优化:考虑分页的分组高宽比总和平衡

虽然上面已经使用算法来平衡两列的高宽比总和了,但是还存在一个问题,即商品数据是分页查询的,比如第第一页查询的结果是第一列的高宽比总和大于第二列的高宽比总和。那么为了可以更好地平衡两列的高宽比总和,第二页数据的查询结果应该是第二列的高宽比总和大于第一列的高宽比总和。为了处理这个问题,在使用回溯算法的时候,需要接收当前已渲染页面的两列宽高比,这样才能方便更好地进行决策

页面代码

从下面的代码中,可以很直观地看到,每次分页查询都更新两列对应地高宽比总和,并在发送请求的时候带上这两个参数

/**
* 查询商品vo集合
*/
listProductVo() {
return new Promise((resolve, reject) => {
	// 设置当前两列的高宽比总和
	this.searchForm.sumAspectRatioOfColumn1 = this.sumAspectRatioOfColumn1;
	this.searchForm.sumAspectRatioOfColumn2 = this.sumAspectRatioOfColumn2;
	listProductVo(this.searchForm, this.page).then(res => {
		// console.log("listProductVo:" + JSON.stringify(res))
		let productVoList = res.data.pageMes.rows;
		this.total = res.data.pageMes.total;
		// this.productList = [
		// 	[],
		// 	[]
		// ];
		// for (var i = 0; i < productVoList.length; i++) {
		// 	if (i % 2 == 0) {
		// 		// 第一列数据
		// 		this.productList[0].push(productVoList[i]);
		// 	} else {
		// 		// 第二列数据
		// 		this.productList[1].push(productVoList[i]);
		// 	}
		// }
		let groups = res.data.groups;
		for (var i = 0; i < groups[0].length; i++) {
			if (groups[0][i].picList != null && groups[0][i].picList.length > 0) {
				this.sumAspectRatioOfColumn1 += groups[0][i].picList[0].aspectRatio;
			}
		}
		for (var i = 0; i < groups[1].length; i++) {
			if (groups[1][i].picList != null && groups[1][i].picList.length > 0) {
				this.sumAspectRatioOfColumn2 += groups[1][i].picList[0].aspectRatio;
			}
		}
		this.productList[0] = this.productList[0].concat(groups[0]);
		this.productList[1] = this.productList[1].concat(groups[1]);
		resolve();
	})

})

},

Controller

/**
 * 查询商品Vo列表
 */
@PreAuthorize("@ss.hasPermi('market:product:list')")
@PostMapping("/listProductVo")
@ApiOperation("获取商品列表")
public AjaxResult listProductVo(@RequestBody ProductVo productVo) {
    startPage();
    if (productVo.getProductCategoryId() != null) {
        // --if-- 当分类不为空的时候,只按照分类来搜索
        productVo.setKeyword(null);
    }
    if (productVo.getIsSearchStar() != null && productVo.getIsSearchStar() == true) {
        productVo.setStarPeopleId(getLoginUser().getUserId());
    }
    List<ProductVo> productVoList = productService.selectProductVoList(productVo);
    // 将productVoList分成两组,要求两组的高度之和相差最小
    List<ProductVo>[] groups = productService.splitToTwoGroups(productVoList, productVo.getSumAspectRatioOfColumn1(), productVo.getSumAspectRatioOfColumn2());
    Map<String, Object> map = new HashMap<>();
    TableDataInfo pageMes = getDataTable(productVoList);
    map.put("pageMes", pageMes);
    map.put("groups", groups);
    return AjaxResult.success(map);
}

Service

@Override
public List<ProductVo>[] splitToTwoGroups(List<ProductVo> productVoList, Double sumAspectRatioOfColumn1, Double sumAspectRatioOfColumn2) {
    List<ProductVo>[] resultArr = new List[2];
    for (int i = 0; i < resultArr.length; i++) {
        resultArr[i] = new ArrayList<>();
    }
    /// 数据准备
    // 获取每个图片的高宽比
    Map<Long, Double> idAndRatioMap = new HashMap<>();
    // 存储所有商品的id
    List<Long> idList = new ArrayList<>();
    long start = System.currentTimeMillis();
    for (ProductVo productVo : productVoList) {
        idList.add(productVo.getId());
        if (productVo.getPicList() != null && productVo.getPicList().size() > 0) {
//                try {
//                    BufferedImage sourceImg = ImageIO.read(new URL(productVo.getPicList().get(0)).openStream());
//                    idAndRatioMap.put(productVo.getId(), sourceImg.getHeight() * 1.0 / sourceImg.getWidth());
//                } catch (IOException e) {
//                    throw new RuntimeException(e);
//                }
            idAndRatioMap.put(productVo.getId(), productVo.getPicList().get(0).getAspectRatio());
        } else {
            idAndRatioMap.put(productVo.getId(), 0.0);
        }
    }
    System.out.println("分组时间:" + (System.currentTimeMillis() - start) + "ms");

    /// 深度优先遍历,找出所有方案,选择两组高度差距最小的分组方案
    GroupDivide groupDivide = new GroupDivide();
    groupDivide.dfsSearch(idList, 0, new ArrayList<>(), idAndRatioMap,sumAspectRatioOfColumn1,sumAspectRatioOfColumn2);

    /// 最后处理分组
    List<Long> group1 = groupDivide.bestGroup1;
    List<Long> group2 = new ArrayList<>();
    for (Long id : idList) {
        if (group1.indexOf(id) == -1) {
            group2.add(id);
        }
    }
    for (ProductVo productVo : productVoList) {
        if (group1.indexOf(productVo.getId()) != -1) {
            resultArr[0].add(productVo);
        } else {
            resultArr[1].add(productVo);
        }
    }

    return resultArr;
}

回溯算法

package com.shm.algorithm;

import com.ruoyi.common.utils.clone.CloneUtil;

import java.util.List;
import java.util.Map;

/**
 * 首页商品数据分组
 *
 * @Author dam
 * @create 2023/8/30 14:12
 */
public class GroupDivide {
    /**
     * 最小间距
     */
    private double minOffSet = Double.MAX_VALUE;
    /**
     * 存储最好的第一组
     */
    public List<Long> bestGroup1 = null;

    public void dfsSearch(List<Long> idList, int begin, List<Long> curGroup1, Map<Long, Double> idAndRatioMap, Double sumAspectRatioOfColumn1, Double sumAspectRatioOfColumn2) {
        if (begin == idList.size()) {
            // 递归完成
            return;
        }
        for (int i = begin; i < idList.size(); i++) {
            curGroup1.add(idList.get(i));
            // 计算组1的长度-组2的长度
            double offSet = calculateGroup1DifHeifGroup2Hei(idList, curGroup1, idAndRatioMap, sumAspectRatioOfColumn1, sumAspectRatioOfColumn2);
            if (offSet > minOffSet) {
                // 如果当前差距已经大于最小差距,执行剪枝,因为如果再往第一组增加图片的话,那差距只会更大,没必要再往下搜索了
                // 删除最后一个元素
                curGroup1.remove(curGroup1.size() - 1);
                continue;
            } else if (Math.abs(offSet) < minOffSet) {
                // 找到更小的间距,保存最优解
                minOffSet = Math.abs(offSet);
                bestGroup1 = CloneUtil.arrayListClone(curGroup1);
            }
            dfsSearch(idList, i + 1, curGroup1, idAndRatioMap, sumAspectRatioOfColumn1, sumAspectRatioOfColumn2);
            // 删除最后一个元素
            curGroup1.remove(curGroup1.size() - 1);
        }
    }

    /**
     * 计算第一组的图片的总高度 减去 第二组图片的总高度
     *
     * @param idList
     * @param group1
     * @param idAndRatioMap
     * @param sumAspectRatioOfColumn1
     * @param sumAspectRatioOfColumn2
     * @return
     */
    private double calculateGroup1DifHeifGroup2Hei(List<Long> idList, List<Long> group1, Map<Long, Double> idAndRatioMap, Double sumAspectRatioOfColumn1, Double sumAspectRatioOfColumn2) {
        // 设置初始值
        double sum1 = sumAspectRatioOfColumn1, sum2 = sumAspectRatioOfColumn2;
        for (Long id : idList) {
            if (group1.indexOf(id) == -1) {
                sum2 += idAndRatioMap.get(id);
            } else {
                sum1 += idAndRatioMap.get(id);
            }
        }
        return sum1 - sum2;
    }

}

优化:考虑商品信息的高宽比

在这里插入图片描述
上面还有一个问题,第二页的数据应该放到第二列更好,解决方式如下,直接根据元素id获取元素的实际高度/实际宽度(即考虑到商品信息的实际高宽比)

<u-row customStyle="margin-top: 10px" gutter="20rpx" align="start"
v-if="productList[0].length>0&&loadData==false">
	<u-col span="6" class="col">
		<view id="view1">
			<view class="productVoItem" v-for="(productVo,index1) in productList[0]" :key="index1"
				@click="seeProductDetail(productVo)">
				<u--image v-if="productVo.picList!=null&&productVo.picList.length>0" :showLoading="true"
					:src="productVo.picList[0].address" width="100%"
					:height="productVo.picList[0].aspectRatio*100+'%'" radius="10" mode="widthFix"
					:lazy-load="true" :fade="true" duration="450"
					@error="reloadPir(productVo.picList[0].address)">
					<!-- 加载失败展示 -->
					<view slot="error" style="font-size: 24rpx;">加载失败</view>
					<!-- 加载中提示 -->
					<template v-slot:loading>
						<view style="height: 100px;width: 100%;">
							<u-loading-icon color="#A2A2A2"></u-loading-icon>
						</view>
					</template>
				</u--image>
				<!-- <u--image v-else :showLoading="true" :src="src" @click="click"></u--image> -->
				<view class="productMes">
					<text class="productName">【{{productVo.name}}】</text>
					<text>
						{{productVo.description==null?'':productVo.description}}
					</text>
				</view>
				<view style="display: flex;align-items: center;">
					<!-- 现价 -->
					<view class="price">¥<text class="number">{{productVo.price}}</text>/{{productVo.unit}}
					</view>
					<view style="width: 10px;"></view>
					<!-- 原价 -->
					<view class="originPrice">¥{{productVo.originalPrice}}/{{productVo.unit}}
					</view>
				</view>
				<view style="display: flex;align-items: center;">
					<u--image :src="productVo.avatar" width="20" height="20" shape="circle"></u--image>
					<view style="width: 10px;"></view>
					<view style="font-size: 30rpx;"> {{productVo.nickname}}</view>
				</view>
			</view>
		</view>
	</u-col>
	<u-col span="6" class="col">
		<view id="view2">
			<view class="productVoItem" v-for="(productVo,index1) in productList[1]" :key="index1"
				@click="seeProductDetail(productVo)">
				<u--image v-if="productVo.picList!=null&&productVo.picList.length>0" :showLoading="true"
					:src="productVo.picList[0].address" width="100%"
					:height="productVo.picList[0].aspectRatio*100+'%'" radius="10" mode="widthFix"
					:lazy-load="true" :fade="true" duration="450"
					@error="reloadPir(productVo.picList[0].address)">
					<!-- 加载失败展示 -->
					<view slot="error" style="font-size: 24rpx;">加载失败</view>
					<!-- 加载中提示 -->
					<template v-slot:loading>
						<view style="height: 100px;width: 100%;">
							<u-loading-icon color="#A2A2A2"></u-loading-icon>
						</view>
					</template>
				</u--image>
				<!-- <u--image v-else :showLoading="true" :src="src" @click="click"></u--image> -->
				<view class="productMes">
					<text class="productName">【{{productVo.name}}】</text>
					<text>
						{{productVo.description==null?'':productVo.description}}
					</text>
				</view>
				<view style="display: flex;align-items: center;">
					<!-- 现价 -->
					<view class="price">¥<text class="number">{{productVo.price}}</text>/{{productVo.unit}}
					</view>
					<view style="width: 10px;"></view>
					<!-- 原价 -->
					<view class="originPrice">¥{{productVo.originalPrice}}/{{productVo.unit}}
					</view>
				</view>
				<view style="display: flex;align-items: center;">
					<u--image :src="productVo.avatar" width="20" height="20" shape="circle"></u--image>
					<view style="font-size: 30rpx;"></view>
					<view> {{productVo.nickname}}</view>
				</view>
			</view>
		</view>
	</u-col>
</u-row>

设置实际的高宽比

/**
* 设置高宽比参数
 */
setParam() {
	return new Promise((resolve, reject) => {
		// select中的参数就如css选择器一样选择元素
		uni.createSelectorQuery().in(this).select("#view1")
			.boundingClientRect((rect) => {
				console.log("rect:" + JSON.stringify(rect));
				//拿到聊天框的高度
				this.searchForm.sumAspectRatioOfColumn1 = rect.height * 1.0 / rect.width;

				uni.createSelectorQuery().in(this).select("#view2")
					.boundingClientRect((rect) => {
						//拿到聊天框的高度
						this.searchForm.sumAspectRatioOfColumn2 = rect.height * 1.0 / rect
							.width;
						resolve();
					})
					.exec();
			})
			.exec();
	})
},

除此之外,后端服务在使用回溯算法的时候,也应该考虑到商品信息的高宽比,由于商品信息中的元素大小都是使用rpx为单位的,因此可以直接计算出商品信息的高宽比,后面将该参数传递给后端即可

// 标题、价格、头像的高宽比 分子、分母的单位都是rpx
messageAspectRatio: (30 + 40 + 32) / ((750 - 20 * 2 - 20) / 2)

Controller

/**
* 查询商品Vo列表
*/
@PreAuthorize("@ss.hasPermi('market:product:list')")
@PostMapping("/listProductVo")
@ApiOperation("获取商品列表")
public AjaxResult listProductVo(@RequestBody ProductVo productVo) {
   startPage();
   if (productVo.getProductCategoryId() != null) {
       // --if-- 当分类不为空的时候,只按照分类来搜索
       productVo.setKeyword(null);
   }
   if (productVo.getIsSearchStar() != null && productVo.getIsSearchStar() == true) {
       productVo.setStarPeopleId(getLoginUser().getUserId());
   }
   List<ProductVo> productVoList = productService.selectProductVoList(productVo);
   Map<String, Object> map = new HashMap<>();
   TableDataInfo pageMes = getDataTable(productVoList);
   map.put("pageMes", pageMes);
   if (productVo.getSumAspectRatioOfColumn1() != null && productVo.getSumAspectRatioOfColumn2() != null) {
       // 将productVoList分成两组,要求两组的高度之和相差最小
       List<ProductVo>[] groups = productService.splitToTwoGroups(productVoList, productVo.getSumAspectRatioOfColumn1(), productVo.getSumAspectRatioOfColumn2(),productVo.getMessageAspectRatio());
       map.put("groups", groups);
   }

   return AjaxResult.success(map);
}

Service

    @Override
    public List<ProductVo>[] splitToTwoGroups(List<ProductVo> productVoList, Double sumAspectRatioOfColumn1, Double sumAspectRatioOfColumn2, Double messageAspectRatio) {
        List<ProductVo>[] resultArr = new List[2];
        for (int i = 0; i < resultArr.length; i++) {
            resultArr[i] = new ArrayList<>();
        }
        /// 数据准备
        // 获取每个图片的高宽比
        Map<Long, Double> idAndRatioMap = new HashMap<>();
        // 存储所有商品的id
        List<Long> idList = new ArrayList<>();
//        long start = System.currentTimeMillis();
        for (ProductVo productVo : productVoList) {
            idList.add(productVo.getId());
            if (productVo.getPicList() != null && productVo.getPicList().size() > 0) {
//                try {
//                    BufferedImage sourceImg = ImageIO.read(new URL(productVo.getPicList().get(0)).openStream());
//                    idAndRatioMap.put(productVo.getId(), sourceImg.getHeight() * 1.0 / sourceImg.getWidth());
//                } catch (IOException e) {
//                    throw new RuntimeException(e);
//                }
                idAndRatioMap.put(productVo.getId(), productVo.getPicList().get(0).getAspectRatio());
            } else {
                idAndRatioMap.put(productVo.getId(), 0.0);
            }
        }
//        System.out.println("分组时间:" + (System.currentTimeMillis() - start) + "ms");

        /// 深度优先遍历,找出所有方案,选择两组高度差距最小的分组方案
        GroupDivide groupDivide = new GroupDivide();
        groupDivide.search(idList, idAndRatioMap, sumAspectRatioOfColumn1, sumAspectRatioOfColumn2, messageAspectRatio);

        /// 最后处理分组
        List<Long> group1 = groupDivide.bestGroup1;
        List<Long> group2 = new ArrayList<>();
        for (Long id : idList) {
            if (group1.indexOf(id) == -1) {
                group2.add(id);
            }
        }
        for (ProductVo productVo : productVoList) {
            if (group1.indexOf(productVo.getId()) != -1) {
                resultArr[0].add(productVo);
            } else {
                resultArr[1].add(productVo);
            }
        }

        return resultArr;
    }

回溯算法

package com.shm.algorithm;

import com.ruoyi.common.utils.clone.CloneUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 首页商品数据分组
 *
 * @Author dam
 * @create 2023/8/30 14:12
 */
public class GroupDivide {
    /**
     * 最小间距
     */
    private double minOffSet = Double.MAX_VALUE;
    /**
     * 存储最好的第一组
     */
    public List<Long> bestGroup1 = null;

    public void search(List<Long> idList, Map<Long, Double> idAndRatioMap, Double sumAspectRatioOfColumn1, Double sumAspectRatioOfColumn2, Double messageAspectRatio) {
        List<Long> curGroup1 = new ArrayList<>();
        // 先搜索组1为空的方案
        double offSet = calculateGroup1DifHeifGroup2Hei(idList, curGroup1, idAndRatioMap, sumAspectRatioOfColumn1, sumAspectRatioOfColumn2, messageAspectRatio);
        if (Math.abs(offSet) < minOffSet) {
            // 找到更小的间距,保存最优解
            minOffSet = Math.abs(offSet);
            bestGroup1 = CloneUtil.arrayListClone(curGroup1);
        }
        // 递归搜索组1不为空的其他方案
        this.dfsSearch(idList, 0, curGroup1, idAndRatioMap, sumAspectRatioOfColumn1, sumAspectRatioOfColumn2,messageAspectRatio);
    }

    /**
     * 深度优先遍历搜索
     * @param idList
     * @param begin
     * @param curGroup1
     * @param idAndRatioMap
     * @param sumAspectRatioOfColumn1
     * @param sumAspectRatioOfColumn2
     * @param messageAspectRatio
     */
    public void dfsSearch(List<Long> idList, int begin, List<Long> curGroup1, Map<Long, Double> idAndRatioMap, Double sumAspectRatioOfColumn1, Double sumAspectRatioOfColumn2,Double messageAspectRatio) {
        if (begin == idList.size()) {
            // 递归完成
            return;
        }
        for (int i = begin; i < idList.size(); i++) {
            curGroup1.add(idList.get(i));
            // 计算组1的长度-组2的长度
            double offSet = calculateGroup1DifHeifGroup2Hei(idList, curGroup1, idAndRatioMap, sumAspectRatioOfColumn1, sumAspectRatioOfColumn2, messageAspectRatio);
            if (offSet > minOffSet) {
                // 如果当前差距已经大于最小差距,执行剪枝,因为如果再往第一组增加图片的话,那差距只会更大,没必要再往下搜索了
                // 删除最后一个元素
                curGroup1.remove(curGroup1.size() - 1);
                continue;
            } else if (Math.abs(offSet) < minOffSet) {
                // 找到更小的间距,保存最优解
                minOffSet = Math.abs(offSet);
                bestGroup1 = CloneUtil.arrayListClone(curGroup1);
            }
            dfsSearch(idList, i + 1, curGroup1, idAndRatioMap, sumAspectRatioOfColumn1, sumAspectRatioOfColumn2,messageAspectRatio);
            // 删除最后一个元素
            curGroup1.remove(curGroup1.size() - 1);
        }
    }

    /**
     * 计算第一组的图片的总高度 减去 第二组图片的总高度
     *
     * @param idList
     * @param group1
     * @param idAndRatioMap
     * @param sumAspectRatioOfColumn1
     * @param sumAspectRatioOfColumn2
     * @param messageAspectRatio
     * @return
     */
    private double calculateGroup1DifHeifGroup2Hei(List<Long> idList, List<Long> group1, Map<Long, Double> idAndRatioMap, Double sumAspectRatioOfColumn1, Double sumAspectRatioOfColumn2, Double messageAspectRatio) {
        // 设置初始值
        double sum1 = sumAspectRatioOfColumn1, sum2 = sumAspectRatioOfColumn2;
        for (Long id : idList) {
            if (group1.indexOf(id) == -1) {
                sum2 += idAndRatioMap.get(id);
                sum2 += messageAspectRatio;
            } else {
                sum1 += idAndRatioMap.get(id);
                sum1 += messageAspectRatio;
            }
        }
        return sum1 - sum2;
    }

}

页面整体代码

【index页面】

<template>
	<view class="content">

		<u-toast ref="uToast"></u-toast>
		<!-- 回到上方按钮 -->
		<u-back-top :scroll-top="scrollTop"></u-back-top>

		<view style="display: flex;align-items: center;">
			<u-search placeholder="请输入商品名称" v-model="searchForm.keyword" @search="listProductVo" :showAction="false"
				:clearabled="true">
			</u-search>
			<text class="iconfont" style="font-size: 35px;" @click="selectCategory()">&#xe622;</text>
		</view>
		<u-row customStyle="margin-top: 10px" gutter="20rpx" align="start"
			v-if="productList[0].length>0&&loadData==false">
			<u-col span="6" class="col">
				<view id="view1">
					<view class="productVoItem" v-for="(productVo,index1) in productList[0]" :key="index1"
						@click="seeProductDetail(productVo)">
						<u--image v-if="productVo.picList!=null&&productVo.picList.length>0" :showLoading="true"
							:src="productVo.picList[0].address" width="100%"
							:height="productVo.picList[0].aspectRatio*100+'%'" radius="10" mode="widthFix"
							:lazy-load="true" :fade="true" duration="450"
							@error="reloadPir(productVo.picList[0].address)">
							<!-- 加载失败展示 -->
							<view slot="error" style="font-size: 24rpx;">加载失败</view>
							<!-- 加载中提示 -->
							<template v-slot:loading>
								<view style="height: 100px;width: 100%;">
									<u-loading-icon color="#A2A2A2"></u-loading-icon>
								</view>
							</template>
						</u--image>
						<!-- <u--image v-else :showLoading="true" :src="src" @click="click"></u--image> -->
						<view class="productMes">
							<text class="productName">【{{productVo.name}}】</text>
							<text>
								{{productVo.description==null?'':productVo.description}}
							</text>
						</view>
						<view style="display: flex;align-items: center;">
							<!-- 现价 -->
							<view class="price">¥<text class="number">{{productVo.price}}</text>/{{productVo.unit}}
							</view>
							<view style="width: 10px;"></view>
							<!-- 原价 -->
							<view class="originPrice">¥{{productVo.originalPrice}}/{{productVo.unit}}
							</view>
						</view>
						<view style="display: flex;align-items: center;">
							<u--image :src="productVo.avatar" width="20" height="20" shape="circle"></u--image>
							<view style="width: 10px;"></view>
							<view style="font-size: 30rpx;"> {{productVo.nickname}}</view>
						</view>
					</view>
				</view>
			</u-col>
			<u-col span="6" class="col">
				<view id="view2">
					<view class="productVoItem" v-for="(productVo,index1) in productList[1]" :key="index1"
						@click="seeProductDetail(productVo)">
						<u--image v-if="productVo.picList!=null&&productVo.picList.length>0" :showLoading="true"
							:src="productVo.picList[0].address" width="100%"
							:height="productVo.picList[0].aspectRatio*100+'%'" radius="10" mode="widthFix"
							:lazy-load="true" :fade="true" duration="450"
							@error="reloadPir(productVo.picList[0].address)">
							<!-- 加载失败展示 -->
							<view slot="error" style="font-size: 24rpx;">加载失败</view>
							<!-- 加载中提示 -->
							<template v-slot:loading>
								<view style="height: 100px;width: 100%;">
									<u-loading-icon color="#A2A2A2"></u-loading-icon>
								</view>
							</template>
						</u--image>
						<!-- <u--image v-else :showLoading="true" :src="src" @click="click"></u--image> -->
						<view class="productMes">
							<text class="productName">【{{productVo.name}}】</text>
							<text>
								{{productVo.description==null?'':productVo.description}}
							</text>
						</view>
						<view style="display: flex;align-items: center;">
							<!-- 现价 -->
							<view class="price">¥<text class="number">{{productVo.price}}</text>/{{productVo.unit}}
							</view>
							<view style="width: 10px;"></view>
							<!-- 原价 -->
							<view class="originPrice">¥{{productVo.originalPrice}}/{{productVo.unit}}
							</view>
						</view>
						<view style="display: flex;align-items: center;">
							<u--image :src="productVo.avatar" width="20" height="20" shape="circle"></u--image>
							<view style="font-size: 30rpx;"></view>
							<view> {{productVo.nickname}}</view>
						</view>
					</view>
				</view>
			</u-col>
		</u-row>
		<!-- 显示加载相关字样 -->
		<u-loadmore v-if="productList[0].length>0&&loadData==false" :status="loadmoreStatus" />

		<u-empty v-if="productList[0].length==0&&loadData==false" mode="data" texColor="#ffffff" iconSize="180"
			iconColor="#D7DEEB" text="所选择的分类没有对应的商品,请重新选择" textColor="#D7DEEB" textSize="18" marginTop="30">
		</u-empty>
		<view style="margin-top: 20px;" v-if="loadData==true">
			<u-skeleton :loading="true" :animate="true" rows="10"></u-skeleton>
		</view>

		<!-- 浮动按钮 -->
		<FloatButton @click="cellMyProduct()">
			<u--image :src="floatButtonPic" shape="circle" width="60px" height="60px"></u--image>
		</FloatButton>
	</view>
</template>

<script>
	import FloatButton from "@/components/FloatButton/FloatButton.vue";
	import {
		listProductVo
	} from "@/api/market/product.js";
	import pictureApi from "@/utils/picture.js";
	import Vue from 'vue';
	import {
		debounce
	} from "@/utils/debounce.js"

	export default {
		components: {
			FloatButton
		},
		onShow: function() {
			let categoryNameList = uni.getStorageSync("categoryNameList");
			if (categoryNameList) {
				this.categoryNameList = categoryNameList;
				this.searchForm.productCategoryId = uni.getStorageSync("productCategoryId");
				this.searchForm.keyword = this.getCategoryLayerName(this.categoryNameList);
				uni.removeStorageSync("categoryNameList");
				uni.removeStorageSync("productCategoryId");
				this.listProductVo();
			}
		},
		data() {
			return {
				title: 'Hello',
				// 浮动按钮的图片
				floatButtonPic: require("@/static/cellLeaveUnused.png"),
				searchForm: {
					// 商品搜索关键词
					keyword: "",
					productCategoryId: undefined,
					sumAspectRatioOfColumn1: 0,
					sumAspectRatioOfColumn2: 0,
					// 标题、价格、头像的高宽比 分子、分母的单位都是rpx
					messageAspectRatio: (30 + 40 + 32) / ((750 - 20 * 2 - 20) / 2)
				},
				productList: [
					[],
					[]
				],
				loadData: false,
				// 用来锁定,防止多次同时进行websocket连接
				lockReconnect: false,
				// 心跳一次间隔的时间,单位毫秒
				heartbeatTime: 5000,
				page: {
					pageNum: 1,
					pageSize: 10
				},
				// 总数据条数
				total: 0,
				// 数据加载状态
				loadmoreStatus: "loadmore",
				// 用来控制滚动到最上方
				scrollTop: 0,
				// 分别存储两列的高宽比总和
				sumAspectRatioOfColumn1: 0,
				sumAspectRatioOfColumn2: 0,
			}
		},
		onLoad() {

		},
		created() {
			this.initWebsocket();
			// this.getMoreProductVo = debounce(this.getMoreProductVo);
		},
		mounted() {
			this.loadData = true;
			this.listProductVo().then(() => {
				this.loadData = false;
			});
		},
		// 监听用户滑动到底部
		onReachBottom() {
			this.getMoreProductVo();
		},
		// 在滑动过程实时获取现在的滚动条位置,并保存当前的滚动条位置
		onPageScroll(e) {
			this.scrollTop = e.scrollTop;
		},
		methods: {
			/**
			 * 查询商品vo集合
			 */
			listProductVo() {
				return new Promise((resolve, reject) => {
					// 设置当前两列的高宽比总和
					// this.searchForm.sumAspectRatioOfColumn1 = this.sumAspectRatioOfColumn1;
					// this.searchForm.sumAspectRatioOfColumn2 = this.sumAspectRatioOfColumn2;

					console.log("this.searchForm:" + JSON.stringify(this.searchForm));
					listProductVo(this.searchForm, this.page).then(res => {
						// console.log("listProductVo:" + JSON.stringify(res))
						let productVoList = res.data.pageMes.rows;
						this.total = res.data.pageMes.total;
						// this.productList = [
						// 	[],
						// 	[]
						// ];
						// for (var i = 0; i < productVoList.length; i++) {
						// 	if (i % 2 == 0) {
						// 		// 第一列数据
						// 		this.productList[0].push(productVoList[i]);
						// 	} else {
						// 		// 第二列数据
						// 		this.productList[1].push(productVoList[i]);
						// 	}
						// }
						let groups = res.data.groups;
						for (var i = 0; i < groups[0].length; i++) {
							if (groups[0][i].picList != null && groups[0][i].picList.length > 0) {
								this.sumAspectRatioOfColumn1 += groups[0][i].picList[0].aspectRatio;
							}
						}
						for (var i = 0; i < groups[1].length; i++) {
							if (groups[1][i].picList != null && groups[1][i].picList.length > 0) {
								this.sumAspectRatioOfColumn2 += groups[1][i].picList[0].aspectRatio;
							}
						}
						this.productList[0] = this.productList[0].concat(groups[0]);
						this.productList[1] = this.productList[1].concat(groups[1]);
						resolve();
					})

				})

			},
			/**
			 * 获取下一页的商品
			 */
			getMoreProductVo() {
				if (this.productList[0].length + this.productList[1].length >= this.total) {
					// this.$refs.uToast.show({
					// 	type: 'warning',
					// 	message: "已经加载完所有商品数据",
					// 	duration: 1000
					// })
				} else {
					if (this.loadData != true) {
						// console.log("--------------------------获取下一页商品---------------------------")
						this.page.pageNum++;
						// 显示正在加载
						this.loadmoreStatus = "loading";
						this.setParam().then(res => {
							this.listProductVo().then(() => {
								if (this.productList[0].length + this.productList[1].length >= this
									.total) {
									// 没有更多了
									this.loadmoreStatus = "nomore";
								} else {
									// 加载更多
									this.loadmoreStatus = "loadmore";
								}
							});
						})
					}
				}
			},
			/**
			 * 设置高宽比参数
			 */
			setParam() {
				return new Promise((resolve, reject) => {
					// select中的参数就如css选择器一样选择元素
					uni.createSelectorQuery().in(this).select("#view1")
						.boundingClientRect((rect) => {
							console.log("rect:" + JSON.stringify(rect));
							//拿到聊天框的高度
							this.searchForm.sumAspectRatioOfColumn1 = rect.height * 1.0 / rect.width;

							uni.createSelectorQuery().in(this).select("#view2")
								.boundingClientRect((rect) => {
									//拿到聊天框的高度
									this.searchForm.sumAspectRatioOfColumn2 = rect.height * 1.0 / rect
										.width;
									resolve();
								})
								.exec();
						})
						.exec();
				})

			},
			/**
			 * 跳转到卖闲置页面
			 */
			cellMyProduct() {
				console.log("我要卖闲置");
				uni.navigateTo({
					url: "/pages/sellMyProduct/sellMyProduct"
				})
			},
			/**
			 * 获取高宽比 乘以 100%
			 */
			getAspectRatio(url) {
				return pictureApi.getAspectRatio(url);
			},
			/**
			 * 选择分类
			 */
			selectCategory() {
				uni.navigateTo({
					url: "/pages/sellMyProduct/selectCategory"
				})
			},
			/**
			 * 获取商品名称
			 */
			getCategoryLayerName() {
				let str = '';
				for (let i = 0; i < this.categoryNameList.length - 1; i++) {
					str += this.categoryNameList[i] + '/';
				}
				return str + this.categoryNameList[this.categoryNameList.length - 1];
			},
			/**
			 * 查看商品的详情
			 */
			seeProductDetail(productVo) {
				// console.log("productVo:"+JSON.stringify(productVo))
				uni.navigateTo({
					url: "/pages/product/detail?productVo=" + encodeURIComponent(JSON.stringify(productVo))
				})
			},
			/**
			 * 重新加载图片
			 */
			reloadPir(pic) {
				console.log("图片加载失败,pic:" + pic)
			},
			/**

			 * 创建websocket连接
			 */
			initWebsocket() {
				console.log("this.socket:" + JSON.stringify(this.$socket))
				// this.$socket == null,刚刚进入首页,还没有建立过websocket连接
				// this.$socket.readyState==0 表示正在连接当中
				// this.$socket.readyState==1 表示处于连接状态
				// this.$socket.readyState==2 表示连接正在关闭
				// this.$socket.readyState==3 表示连接已经关闭
				if (this.$socket == null || (this.$socket.readyState != 1 && this.$socket.readyState != 0)) {
					this.$socket = uni.connectSocket({
						url: "ws://10.23.17.146:8085/websocket/" + uni.getStorageSync("curUser").userName,
						success(res) {
							console.log('WebSocket连接成功', res);
						},
					})
					// console.log("this.socket:" + this.$socket)

					// 监听WebSocket连接打开事件
					this.$socket.onOpen((res) => {
						console.log("websocket连接成功")
						Vue.prototype.$socket = this.$socket;
						// 连接成功,开启心跳
						this.headbeat();
					});
					// 连接异常
					this.$socket.onError((res) => {
						console.log("websocket连接出现异常");
						// 重连
						this.reconnect();
					})
					// 连接断开
					this.$socket.onClose((res) => {
						console.log("websocket连接关闭");
						// 重连
						this.reconnect();
					})
				}
			},
			/**
			 * 重新连接
			 */
			reconnect() {
				// console.log("重连");
				// 防止重复连接
				if (this.lockReconnect == true) {
					return;
				}
				// 锁定,防止重复连接
				this.lockReconnect = true;
				// 间隔一秒再重连,避免后台服务出错时,客户端连接太频繁
				setTimeout(() => {
					this.initWebsocket();
				}, 1000)
				// 连接完成,设置为false
				this.lockReconnect = false;
			},
			// 开启心跳
			headbeat() {
				// console.log("websocket心跳");
				var that = this;
				setTimeout(function() {
					if (that.$socket.readyState == 1) {
						// websocket已经连接成功
						that.$socket.send({
							data: JSON.stringify({
								status: "ping"
							})
						})
						// 调用启动下一轮的心跳
						that.headbeat();
					} else {
						// websocket还没有连接成功,重连
						that.reconnect();
					}
				}, that.heartbeatTime);
			},
			/**
			 * 返回方法
			 */
			back() {

			}
		}
	}
</script>

<style lang="scss">
	.content {
		padding: 20rpx;

		.col {
			width: 50%;
		}

		.productVoItem {
			margin-bottom: 20px;

			.productMes {
				overflow: hidden;
				text-overflow: ellipsis;
				display: -webkit-box;
				/* 显示2行 */
				-webkit-line-clamp: 1;
				-webkit-box-orient: vertical;
				font-size: 32rpx;

				.productName {
					font-weight: bold;
				}
			}

			.price {
				color: #F84442;
				font-weight: bold;

				.number {
					font-size: 40rpx;
				}
			}

			.originPrice {
				color: #A2A2A2;
				font-size: 15px;
				// 给文字添加中划线
				text-decoration: line-through;
			}
		}
	}
</style>

【上传销售商品页面】

<template>
	<view class="container">
		<u-toast ref="uToast"></u-toast>
		<view class="content">
			<view class="item">
				<view class="labelName">商品名称</view>
				<u--input placeholder="请输入商品名称" border="surround" v-model="product.name"></u--input>
			</view>
			<u-divider text="商品描述和外观"></u-divider>
			<!-- 商品描述 -->
			<u--textarea v-model="product.description" placeholder="请输入商品描述" height="150"></u--textarea>
			<!-- 图片上传 -->
			<view>
				<imageUpload v-model="picList" maxCount="9"></imageUpload>
			</view>

			<u-divider text="分类选择/自定义标签"></u-divider>
			<!-- 分类选择/自定义标签 -->
			<view class="item">
				<view class="labelName">分类</view>
				<view class="selectTextClass" @click="selectCategory">{{getCategoryLayerName()}}</view>
			</view>
			<!-- 商品的属性 新度 功能完整性 -->
			<view class="item">
				<view class="labelName">成色</view>
				<view class="columnClass">
					<view :class="product.fineness==index?'selectTextClass':'textClass'"
						v-for="(finessName,index) in finenessList" :key="index" @click="changeFineness(index)">
						{{finessName}}
					</view>
				</view>
			</view>
			<view class="item">
				<view class="labelName">功能状态</view>
				<view class="columnClass">
					<view :class="product.functionalStatus==index?'selectTextClass':'textClass'"
						v-for="(functionName,index) in functionList" :key="index"
						@click="changeFunctionalStatus(index)">{{functionName}}
					</view>
				</view>
			</view>
			<u-row customStyle="margin-bottom: 10px">
				<u-col span="5">
					<view class="item">
						<view class="labelName">数量</view>
						<u--input placeholder="请输入商品数量" border="surround" v-model="product.number"></u--input>
					</view>
				</u-col>
				<u-col span="7">
					<view class="item">
						<view class="labelName">计量单位</view>
						<u--input placeholder="请输入计量单位" border="surround" v-model="product.unit"></u--input>
					</view>
				</u-col>
			</u-row>

			<!-- 价格 原价 现价 -->
			<u-divider text="价格"></u-divider>
			<u-row customStyle="margin-bottom: 10px">
				<u-col span="6">
					<view class="item">
						<view class="labelName">原价</view>
						<u-input placeholder="请输入原价" border="surround" v-model="product.originalPrice" color="#ff0000"
							@blur="originalPriceChange">
							<u--text text="¥" slot="prefix" margin="0 3px 0 0" type="error"></u--text>
						</u-input>
					</view>
				</u-col>
				<u-col span="6">
					<view class="item">
						<view class="labelName">出售价格</view>
						<u-input placeholder="请输入出售价格" border="surround" v-model="product.price" color="#ff0000"
							@blur="priceChange">
							<u--text text="¥" slot="prefix" margin="0 3px 0 0" type="error"></u--text>
						</u-input>
					</view>
				</u-col>
			</u-row>

			<u-button text="出售" size="large" type="primary" @click="uploadSellProduct"></u-button>
		</view>
	</view>
</template>

<script>
	import imageUpload from "@/components/ImageUpload/ImageUpload.vue";
	import {
		uploadSellProduct
	} from "@/api/market/product.js"
	export default {
		components: {
			imageUpload
		},
		onShow: function() {
			let categoryNameList = uni.getStorageSync("categoryNameList");
			if (categoryNameList) {
				this.categoryNameList = categoryNameList;
				this.product.productCategoryId = uni.getStorageSync("productCategoryId");
				uni.removeStorageSync("categoryNameList");
				uni.removeStorageSync("productCategoryId");
			}
		},
		data() {
			return {
				product: {
					name: '',
					descripption: '',
					picList: [],
					productCategoryId: undefined,
					number: 1,
					unit: '个',
					isContribute: 0,
					originalPrice: 0.00,
					price: 0.00,
					// 成色
					fineness: 0,
					// 功能状态
					functionalStatus: 0,
					brandId: 0
				},
				value: 'dasdas',
				categoryNameList: ["选择分类"],
				finenessList: ["全新", "几乎全新", "轻微使用痕迹", "明显使用痕迹", "外观破损"],
				functionList: ["功能完好无维修", "维修过,可正常使用", "有小问题,不影响使用", "无法正常使用"],
				picList: [],
			}
		},
		methods: {
			getCategoryLayerName() {
				let str = '';
				for (let i = 0; i < this.categoryNameList.length - 1; i++) {
					str += this.categoryNameList[i] + '/';
				}
				return str + this.categoryNameList[this.categoryNameList.length - 1];
			},
			/**
			 * 价格校验
			 * @param {Object} price 价格
			 */
			priceVerify(price) {
				if (isNaN(price)) {
					this.$refs.uToast.show({
						type: 'error',
						message: "输入的价格不是数字,请重新输入"
					})
					return false;
				}
				if (price < 0) {

					this.$refs.uToast.show({
						type: 'error',
						message: "输入的价格不能为负数,请重新输入"
					})
					return false;
				}
				if (price.toString().indexOf('.') !== -1 && price.toString().split('.')[1].length > 2) {
					this.$refs.uToast.show({
						type: 'error',
						message: "输入的价格小数点后最多只有两位数字,请重新输入"
					})
					return false;
				}
				return true;
			},
			originalPriceChange() {
				let haha = this.priceVerify(this.product.originalPrice);
				if (haha === false) {
					console.log("haha:" + haha);
					this.product.originalPrice = 0.00;
					console.log("this.product" + JSON.stringify(this.product));
				}
			},
			priceChange() {
				if (this.priceVerify(this.product.price) === false) {
					this.product.price = 0.00;
				}
			},
			/**
			 * 修改成色
			 * @param {Object} index
			 */
			changeFineness(index) {
				this.product.fineness = index;
			},
			/**
			 * 修改功能状态
			 * @param {Object} index
			 */
			changeFunctionalStatus(index) {
				this.product.functionalStatus = index;
			},
			/**
			 * 上传闲置商品
			 */
			uploadSellProduct() {
				// console.log("上传闲置商品picList:" + JSON.stringify(this.picList));
				if (this.product.productCategoryId) {
					if (this.picList.length == 0) {
						this.$refs.uToast.show({
							type: 'error',
							message: "商品图片没有上传成功"
						})
					} else {
						this.setPicAspectRatio().then(() => {
							// console.log("即将上传的商品:" + JSON.stringify(this.product));
							uploadSellProduct(this.product).then(res => {
								this.$refs.uToast.show({
									type: 'success',
									message: "您的商品已经发布到平台"
								})
								setTimeout(() => {
									uni.reLaunch({
										url: "/pages/index/index"
									})
								}, 1000)
							}).catch(error => {
								console.log("error:" + JSON.stringify(error));
								this.$refs.uToast.show({
									type: 'error',
									message: "商品发布失败"
								})
							});
						});

					}
				} else {
					this.$refs.uToast.show({
						type: 'error',
						message: "请选择分类"
					})
				}
			},
			/**
			 * 设置图片的宽高比
			 */
			setPicAspectRatio() {
				return new Promise((resolve, reject) => {
					this.product.picList = [];
					let promises = [];
					for (let i = 0; i < this.picList.length; i++) {
						let picUrl = this.picList[i];
						promises.push(this.getAspectRatio(picUrl).then((res) => {
							let pic = {
								address: picUrl,
								aspectRatio: res
							}
							this.product.picList.push(pic);
							console.log("当前图片高宽比设置完成");
						}))
					}
					Promise.all(promises).then(() => {
						console.log("所有图片高宽比设置完成,this.product.picList:" + JSON.stringify(this.product
							.picList));
						resolve();
					})
				})
			},
			/**
			 * 获取单个图片的高宽比

			 * @param {Object} url
			 */
			getAspectRatio(url) {
				return new Promise((resolve, reject) => {
					uni.getImageInfo({
						src: url,
						success: function(res) {
							let aspectRatio = res.height / res.width;
							resolve(aspectRatio);
						}
					});
				})
			},

			/**
			 * 选择分类
			 */
			selectCategory() {
				uni.navigateTo({
					url: "/pages/sellMyProduct/selectCategory"
				})
			}
		}
	}
</script>

<style lang="scss">
	.container {
		background: #F6F6F6;
		min-height: 100vh;
		padding: 20rpx;

		.content {
			background: #ffffff;
			padding: 20rpx;


			.item {
				display: flex;
				align-items: center;
				height: 50px;
				margin-bottom: 5px;

				.labelName {
					width: 70px;
					margin-right: 10px;
				}

				.textClass {
					display: inline;
					background: #F7F7F7;
					padding: 10px;
					margin-right: 15px;
					border-radius: 5px;
				}

				.selectTextClass {
					display: inline;
					background: #2B92FF;
					padding: 10px;
					margin-right: 15px;
					border-radius: 5px;
					color: #ffffff;
					font-weight: bold;
				}

				.columnClass {
					// height: 50px;
					display: flex;
					align-items: center;

					width: calc(100% - 70px);
					overflow-x: auto;
					// // 让内容只有一行
					white-space: nowrap;
				}

				.columnClass::-webkit-scrollbar {
					background-color: transparent;
					/* 设置滚动条背景颜色 */
					// width: 0px;
					height: 0px;
				}

			}


		}
	}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/966339.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

气象站在日常生活中的重要性

气象站在我们的日常生活中起着重要的作用&#xff0c;它监测着天气的变化&#xff0c;能够提供及时、准确的天气信息&#xff0c;对我们的生产和生活都有着极大的影响。 一、气象站的工作原理 气象站通过一系列传感器设备&#xff0c;对风速、风向、温度、湿度、气压、雨量等…

华为数通方向HCIP-DataCom H12-821题库(单选题:241-261)

第241题 ​​LS Request​​报文不包括以下哪一字段? A、通告路由器(Advertising Router) B、链路状态 ID (Link Srate ID) C、数据库描述序列号(Database Dascription Sequence lumber) D、链路状态类型 Link state type) 答案:C 解析: LS Request 报文中包括以下字段…

将qt6编写的简单小程序烧录Android手机中,以及编译生成apk位置

准备工作 1、一个qt能够编译通过的简单工程。参考&#xff1a;https://www.bilibili.com/video/BV1tp4y1i7EJ?p15 2、配置好QT6的Android环境&#xff0c;(JDK,SDK,NDK等)&#xff0c;可参考&#xff1a;https://mar-sky.blog.csdn.net/article/details/132630567 3、一台闲置…

【计算机硬件CPU】

【计算机硬件CPU】 1、计算机硬件的五大单元2、一切设计的起点&#xff1a; CPU 的架构3、精简指令集 &#xff08;Reduced Instruction Set Computer, RISC&#xff09;4、复杂指令集&#xff08;Complex Instruction Set Computer, CISC&#xff09;5、例题&#xff1a;最新的…

Spring boot 第一个程序

新建工程 选择spring-boot版本 右键创建类TestController&#xff1a; 代码如下&#xff1a; package com.example.demo; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springf…

面经:安卓学习笔记

文章目录 1. Android系统架构2. Activity2.0 定义2.1 生命周期2.2 生命状态2.3 启动模式 3. Service3.1 定义3.2 两种启动方式3.3 生命周期3.4 跨进程service3.5 IntentService 4. BroadCastReceiver4.1 概念4.2 组成4.3 广播接收器的分类4.4 生命周期4.5 静态注册和动态注册 5…

一个.NET 7 + DDD + CQRS +React+Vite的实战项目

项目简介 基于SignalR实现聊天通信&#xff0c;支持横向扩展&#xff0c;可支撑上万用户同时在线聊天 快速体验 http://server.tokengo.top:8888/ 可在这里快速体验使用&#xff0c;请注意目前只适配了PC端&#xff0c;请勿使用手机访问&#xff0c;可能出现样式不适应的情况…

向量数据库Annoy和Milvus

Annoy 和 Milvus 都是用于向量索引和相似度搜索的开源库&#xff0c;它们可以高效地处理大规模的向量数据。 Annoy&#xff08;Approximate Nearest Neighbors Oh Yeah&#xff09;&#xff1a; Annoy 是一种近似最近邻搜索算法&#xff0c;它通过构建一个树状结构来加速最近…

CSS中如何实现文字描边效果(Text Stroke)?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 文字描边效果&#xff08;Text Stroke&#xff09;⭐ 示例⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个…

zookeeper 集群

zookeeper 集群 1、zookeeper 集群说明 initLimit 是Zookeeper用它来限定集群中的Zookeeper服务器连接到Leader的时限 syncLimit 限制了follower服务器与leader服务器之间请求和应答之间的时限 服务器名称与地址&#xff1a;集群信息&#xff08;服务器编号&#xff0c;服务器…

【Java 基础篇】Java 面向对象详解:面向对象编程的艺术

如果你正在学习Java编程&#xff0c;面向对象编程&#xff08;Object-Oriented Programming&#xff0c;OOP&#xff09;是一个不可或缺的概念。Java是一种面向对象的编程语言&#xff0c;这意味着它的编程范式基于对象、类和继承。在本篇博客中&#xff0c;我们将深入探讨Java…

IIR滤波器算法

IIR&#xff08;Infinite Impulse Response&#xff09;滤波器是一类递归型数字滤波器&#xff0c;其输出信号不仅与当前的输入信号有关&#xff0c;还与之前的输入和输出信号有关。因此&#xff0c;IIR滤波器的阶数相对较低&#xff0c;可以实现更为复杂的频率响应。 IIR滤波…

C++包含整数各位重组

void 包含整数各位重组() {//缘由https://bbs.csdn.net/topics/395402016int shu 100000, bs 4, bi shu * bs, a 0, p 0, d 0;while (shu < 500000)if (a<6 && (p to_string(shu).find(to_string(bi)[a], p)) ! string::npos && (d to_string(bi…

2PCNet:昼夜无监督域自适应目标检测(附原代码)

点击蓝字 关注我们 关注并星标 从此不迷路 计算机视觉研究院 公众号ID&#xff5c;计算机视觉研究院 学习群&#xff5c;扫码在主页获取加入方式 计算机视觉研究院专栏 Column of Computer Vision Institute 由于缺乏夜间图像注释&#xff0c;夜间目标检测是一个具有挑战性的问…

反转了,中国发售5G手机,ASML就出售先进光刻机,跪地求饶

日前据《中国日报》等多家知名媒体确认&#xff0c;ASML已获得荷兰许可出售先进的2000i光刻机&#xff0c;这款光刻机可用于生产7纳米工艺&#xff0c;而就在数天前一家中国手机企业发布了国产化的5G手机&#xff0c;采用国产的5G芯片和5G射频芯片&#xff0c;这实在太巧合了。…

Linux持续学习者的必备工具:文本处理神器awk

引言 作为一名Linux持续学习者&#xff0c;我们经常需要处理各种各样的文本文件&#xff0c;例如日志文件、配置文件等。而对于大规模的文本数据&#xff0c;手动处理往往效率低下且容易出错。那么&#xff0c;有没有一种快速而强大的工具可以帮助我们进行文本处理呢&#xff1…

废品回收功能文档

废品回收 基础版 后台功能 功能字段描述二级分类表字段&#xff1a;图标、名称、描述、图片、注意事项、上一级、状态功能&#xff1a;前端展示和筛选&#xff1b;增删改查今日指导价表字段&#xff1a;关联分类、名称、价格、单位、状态功能&#xff1a;前端展示和预估价格&…

基于java+springboot+vue的考研资讯平台-lw

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; ssm mybatis Maven mysql5.7或8.0等等组成&#xff0c;B…

Windows Network File System Remote Code Execution Vulnerability

文章目录 NFS(Network File System)漏洞描述攻击者如何利用此漏洞&#xff1f;该漏洞的危险程度机密性-high真实性-high可用性-high 如何降低漏洞风险推荐阅读 NFS(Network File System)漏洞描述 Name Microsoft Windows Network File System Remote Code Execution Vulnerabi…

IM即时聊天项目

目录 IM即时聊天项目WebSocket 原理搭建环境设置代理创建环境配置驱动&#xff08;搭建环境需要的驱动&#xff09;conf&#xff08;配置信息&#xff09;cache&#xff08;redis&#xff09;model&#xff08;数据库连接&#xff09; 用户注册serializermodelserviceapirouter…