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

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

说明

之前已经在【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);
      }
    });
  })
},

修改商品发布页面的代码

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

/**
* 上传闲置商品
*/
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>


目录
相关文章
|
18天前
|
存储 算法 安全
2024重生之回溯数据结构与算法系列学习之串(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】
数据结构与算法系列学习之串的定义和基本操作、串的储存结构、基本操作的实现、朴素模式匹配算法、KMP算法等代码举例及图解说明;【含常见的报错问题及其对应的解决方法】你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
2024重生之回溯数据结构与算法系列学习之串(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】
|
18天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习(8)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
18天前
|
存储 算法 安全
2024重生之回溯数据结构与算法系列学习之顺序表【无论是王道考研人还真爱粉都能包会的;不然别给我家鸽鸽丢脸好嘛?】
顺序表的定义和基本操作之插入;删除;按值查找;按位查找等具体详解步骤以及举例说明
|
18天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
18天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
18天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
18天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之顺序表习题精讲【无论是王道考研人还真爱粉都能包会的;不然别给我家鸽鸽丢脸好嘛?】
顺序表的定义和基本操作之插入;删除;按值查找;按位查找习题精讲等具体详解步骤以及举例说明
|
18天前
|
存储 算法 安全
2024重生之回溯数据结构与算法系列学习【无论是王道考研人还真爱粉都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构的基本概念;算法的基本概念、特性以及时间复杂度、空间复杂度等举例说明;【含常见的报错问题及其对应的解决方法】
|
1月前
|
算法 Java Linux
java制作海报五:java 后端整合 echarts 画出 折线图,项目放在linux上,echarts图上不显示中文,显示方框口口口
这篇文章介绍了如何在Java后端整合ECharts库来绘制折线图,并讨论了在Linux环境下ECharts图表中文显示问题。
39 1
|
18天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之王道第2.3章节之线性表精题汇总二(5)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
IKU达人之数据结构与算法系列学习×单双链表精题详解、数据结构、C++、排序算法、java 、动态规划 你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!