[簡単ミニ プログラム プロジェクト] ミニ プログラムのホームページが改善されました (下にスライドしてデータを反転し、上に戻り、バックトラッキング アルゴリズムに基づいて 2 つの列のデータの高さと幅の比率のバランスをとります) [バックエンドRuoyi管理システムに基づいて開発されています]

説明する

ホームページの実装については、以前こちらの記事【UniApp開発ミニプログラム】ミニプログラムホームページ(商品表示、商品検索、商品カテゴリー検索)【Ruoyi管理システムによるバックエンド開発】で紹介しましたが、プロジェクト開発は継続的なプロセスであり、この種のプロジェクトを開発するのは初めてであるため、多くの詳細が事前に明確に構想されていないため、この記事は、プロジェクトの一部を最適化するための補足として機能します。以前の問題。

詳細1: ホームページを一番下にスライドすると、次のページで商品を確認する必要があります。

インターフェースのプレビュー

下をスライドすると、下に「Loading」の文字が表示され、次のページの商品データを取得するリクエストがバックエンドに送信されます。

ここに画像の説明を挿入します
すべての製品がロードされると、「No more」という文字が表示されます。

ここに画像の説明を挿入します

ページの実装

次のメソッドは、ユーザーによるページの一番下へのスライドを監視できます。一番下にスライドすると、メソッドを呼び出してさらに多くの製品をクエリします。

// 监听用户滑动到底部
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";
				}
			});
		}
	}
},

詳細 2: ページを下にスライドすると、トップに戻るフローティング ボタンが表示されます。

ラベルを追加する

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

ラベルは変数にバインドされているため、宣言する必要があります。

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

また、スクロール位置をリアルタイムに記録することも必要です

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

詳細 3: 製品の内訳

説明する

前回の記事では、列の並べ替えを実現するために最も簡単な方法、つまり製品配列を直接走査し、製品を最初と 2 番目の列に順番に並べ替える方法を使用しましたが、これでは 2 つの列の製品が高度にソートされてしまいます。したがって、以下に示すように
ここに画像の説明を挿入します
、視覚効果を向上させるために、商品コンテンツの 2 つの列の高さのバランスをとるようにグループ化戦略を変更する必要があります。この質問は次のように理解できます。假设有十个物品,每个物品的长度不太一样,要求将这些物品分到两组中,最后两组物品长度总和最接近,请问需要怎么来分这两个组?

最適化前後の効果の比較

ここに画像の説明を挿入します

バックトラッキングアルゴリズムを使用して実装

ページング クエリが使用され、毎回返されるデータの量は多くないため、バックトラッキング アルゴリズムを直接使用してすべてのグループ化状況を取得し、最終的に高さの差が最も小さいグループ化スキームを選択できます。

コントローラ

/**
 * 查询商品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);
}

サービス

@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;
    }

}

最適化: 画像に対するネットワーク リクエストを削減します。

画像のアスペクト比は不変であるため、属性としてデータ テーブルに保存できます。この方法では、クエリを実行するだけで済みます。取得するためにネットワーク リクエストを使用する必要はありませんが、アスペクト比を取得する必要があります。画像をデータテーブルに保存する前に取得するアスペクト比を取得し、この属性を保存します

データテーブルにフィールドを追加する

ここに画像の説明を挿入します

データテーブル内の既存データのアスペクト比を計算し、データテーブルに更新します

データ テーブルには一部の画像データがすでに存在しているため、アプレットを正しく実行するには、このデータ バッチを修復する必要があります。つまり、各画像のアスペクト比を補う必要があります。データ テーブル内のデータの量は大きくなく、これは 1 回限りのタスクであるため、毎回 1 つのデータを変更するだけで済みます。データ量が大きい場合は、マルチスレッドとバッチ変更を使用して修復速度を最適化できます。

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

製品リリースページのコードを変更する

ここで、データ テーブルに画像のアスペクト比を保存する必要があります。サーバーは画像を保存する前にアスペクト比を直接計算できますが、それでも大量のネットワーク リクエストを送信する必要があり、インターフェイスの同時パフォーマンスに影響します。クライアントが高さと幅を計算して比較し、サーバーに直接アップロードすることをお勧めします。サーバーはデータを直接保存できます。

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

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

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

注意点:

  • getAspectRatio メソッドは画像のアスペクト比を取得してネットワーク要求を送信するため、解決する前に Promise を使用してアスペクト比が正常に取得されたことを確認します。
  • 商品をアップロードする前に、商品に対応するすべての画像のアスペクト比を設定する必要があります。複数の画像がある場合は、すべての画像のアスペクト比が設定されるまで待つ必要があります。この記事では、Promise.all(promises) を使用して、解決する前にすべての画像のアスペクト比が設定されるのを待ちます。

サービスの改善

画像のアスペクト比はデータ テーブルに保存されているため、ネットワーク リクエストを送信する必要はなく、属性値を直接取得するだけで済みます。

@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;
}

[テスト]
ネットワーク リクエストを送信する必要がなくなると、画像のアスペクト比を取得する時間が大幅に短縮されることがわかります。

ここに画像の説明を挿入します

最適化: ページネーションのグループ化されたアスペクト比の合計バランスを考慮する

上記のアルゴリズムは 2 つの列のアスペクト比の合計のバランスをとるために使用されていますが、製品データがページ単位でクエリされるという問題がまだあります。たとえば、最初のページのクエリの結果は次のようになります。最初の列のアスペクト比の合計が 2 番目の列のアスペクト比よりも大きいこと。列のアスペクト比の合計。したがって、2 つの列のアスペクト比の合計のバランスをより良くするには、データの 2 ページ目のクエリ結果は、2 番目の列のアスペクト比の合計が、データのアスペクト比の合計より大きくなる必要があります。最初の列。この問題に対処するには、バックトラッキング アルゴリズムを使用するときに、より適切な決定をより簡単に行うことができるように、現在レンダリングされているページの 2 列のアスペクト比を受け取る必要があります。

ページコード

以下のコードから、各ページング クエリが 2 つの列に対応する高さと幅の比率の合計を更新し、リクエストの送信時にこれら 2 つのパラメーターをもたらすことが直感的にわかります。

/**
* 查询商品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();
	})

})

},

コントローラ

/**
 * 查询商品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);
}

サービス

@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;
    }

}

最適化: 製品情報のアスペクト比を考慮する

ここに画像の説明を挿入します
上記の他にも問題があります。データを 2 ページ目の 2 列目に配置する方がよいでしょう。解決策は次のとおりです。要素 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)

コントローラ

/**
* 查询商品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);
}

サービス

    @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;
    }

}

ページ全体のコード

【インデックスページ】

<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>

同じプロジェクトに関する他の記事

このプロジェクトのその他の記事は、 【簡単ミニプログラムプロジェクト】プロジェクト紹介、ミニプログラムページ表示、連載記事集をご覧ください。

おすすめ

転載: blog.csdn.net/laodanqiu/article/details/132584340