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

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

说明

之前已经在【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>


目录
相关文章
|
16天前
|
小程序 前端开发 API
小程序全栈开发中的多端适配与响应式布局
【4月更文挑战第12天】本文探讨了小程序全栈开发中的多端适配与响应式布局。多端适配涉及平台和设备适应,确保统一用户体验;响应式布局利用媒体查询和弹性布局维持不同设备的布局一致性。实践中,开发者可借助跨平台框架实现多平台开发,运用响应式布局技术适应不同设备。同时,注意兼容性、性能优化和用户体验,以提升小程序质量和用户体验。通过这些方法,开发者能更好地掌握小程序全栈开发。
|
16天前
|
小程序 前端开发 API
微信小程序全栈开发中的异常处理与日志记录
【4月更文挑战第12天】本文探讨了微信小程序全栈开发中的异常处理和日志记录,强调其对确保应用稳定性和用户体验的重要性。异常处理涵盖前端(网络、页面跳转、用户输入、逻辑异常)和后端(数据库、API、业务逻辑)方面;日志记录则关注关键操作和异常情况的追踪。实践中,前端可利用try-catch处理异常,后端借助日志框架记录异常,同时采用集中式日志管理工具提升分析效率。开发者应注意安全性、性能和团队协作,以优化异常处理与日志记录流程。
|
16天前
|
小程序 安全 数据安全/隐私保护
微信小程序全栈开发中的身份认证与授权机制
【4月更文挑战第12天】本文探讨了微信小程序全栈开发中的身份认证与授权机制。身份认证包括手机号验证、微信登录和第三方登录,而授权机制涉及角色权限控制、ACL和OAuth 2.0。实践中,开发者可利用微信登录获取用户信息,集成第三方登录,以及实施角色和ACL进行权限控制。注意点包括安全性、用户体验和合规性,以保障小程序的安全运行和良好体验。通过这些方法,开发者能有效掌握小程序全栈开发技术。
|
16天前
|
小程序 前端开发 安全
小程序全栈开发中的跨域问题及其解决方案
【4月更文挑战第12天】本文探讨了小程序全栈开发中的跨域问题及其解决方案。跨域问题源于浏览器安全策略,主要体现在前后端分离、第三方服务集成和数据共享上。为解决此问题,开发者可采用CORS、JSONP、代理服务器、数据交换格式和域名策略等方法。实践中需注意安全性、兼容性和性能。通过掌握这些解决方案,开发者能更好地处理小程序的跨域问题,提升用户体验。
|
16天前
|
JavaScript 前端开发 小程序
微信小程序全栈开发之性能优化策略
【4月更文挑战第12天】本文探讨了微信小程序全栈开发的性能优化策略,包括前端的资源和渲染优化,如图片压缩、虚拟DOM、代码分割;后端的数据库和API优化,如索引创建、缓存使用、RESTful API设计;以及服务器的负载均衡和CDN加速。通过这些方法,开发者可提升小程序性能,优化用户体验,增强商业价值。
|
16天前
|
小程序 前端开发 JavaScript
微信小程序全栈开发中的PWA技术应用
【4月更文挑战第12天】本文探讨了微信小程序全栈开发中PWA技术的应用,PWA结合Web的开放性和原生应用的性能,提供离线访问、后台运行、桌面图标和原生体验。开发者可利用Service Worker实现离线访问,Worker处理后台运行,Web App Manifest添加桌面图标,CSS和JavaScript提升原生体验。实践中需注意兼容性、性能优化和用户体验。PWA技术能提升小程序的性能和用户体验,助力开发者打造优质小程序。
|
1天前
|
数据采集 算法 数据可视化
MATLAB、R用改进Fuzzy C-means模糊C均值聚类算法的微博用户特征调研数据聚类研究
MATLAB、R用改进Fuzzy C-means模糊C均值聚类算法的微博用户特征调研数据聚类研究
|
2天前
|
机器学习/深度学习 数据采集 算法
共享单车需求量数据用CART决策树、随机森林以及XGBOOST算法登记分类及影响因素分析
共享单车需求量数据用CART决策树、随机森林以及XGBOOST算法登记分类及影响因素分析
|
3天前
|
移动开发 算法 数据可视化
数据分享|Spss Modeler关联规则Apriori模型、Carma算法分析超市顾客购买商品数据挖掘实例
数据分享|Spss Modeler关联规则Apriori模型、Carma算法分析超市顾客购买商品数据挖掘实例
|
3天前
|
机器学习/深度学习 自然语言处理 算法
【视频】K近邻KNN算法原理与R语言结合新冠疫情对股票价格预测|数据分享(下)
【视频】K近邻KNN算法原理与R语言结合新冠疫情对股票价格预测|数据分享
10 0